<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
    <meta charset="utf-8">
<title>Web开发 - wanzixin</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.css">


<link href="/Study/Java/zh-cn/Web%E5%BC%80%E5%8F%91/" rel="alternate" hreflang="en" />
    


<meta name="description" content="">





    <meta name="description" content="从本章开始我们就进入到JavaEE的领域。JavaEE并不是一个软件产品，它更多的是一种软件架构和设计思想。我们可以把JavaEE看作是在JavaSE的基础上，开发的一系列基于服务器的组件、API标准和通用架构。">
<meta property="og:type" content="article">
<meta property="og:title" content="Web开发">
<meta property="og:url" content="https://wanzixin.github.io/Study/Java/zh-cn/Web%E5%BC%80%E5%8F%91/index.html">
<meta property="og:site_name" content="wanzixin">
<meta property="og:description" content="从本章开始我们就进入到JavaEE的领域。JavaEE并不是一个软件产品，它更多的是一种软件架构和设计思想。我们可以把JavaEE看作是在JavaSE的基础上，开发的一系列基于服务器的组件、API标准和通用架构。">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2021-06-16T02:16:47.000Z">
<meta property="article:modified_time" content="2021-06-26T03:01:31.197Z">
<meta property="article:author" content="wanzixin">
<meta name="twitter:card" content="summary">





<link rel="icon" href="/favicon.png">


<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Ovo|Source+Code+Pro">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/css/lightgallery.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/css/justifiedGallery.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/atom-one-light.min.css">


<link rel="stylesheet" href="/css/style.css">


<script defer src="//use.fontawesome.com/releases/v5.0.8/js/all.js"></script>


    
    
    
    
    
    
    
    
    
    

    


<meta name="generator" content="Hexo 5.4.0"></head>
<body>
    
<nav class="navbar is-transparent is-fixed-top navbar-main" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a class="navbar-item navbar-logo" href="/zh-cn">
                
                    
                    wanzixin
                    
                
            </a>
            <div class="navbar-burger">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
        
        <div class="navbar-menu navbar-start">
            
            <a class="navbar-item "
               href="/archives">Archives</a>
            
            <a class="navbar-item "
               href="/categories">Categories</a>
            
            <a class="navbar-item "
               href="/categories/Diary">Diary</a>
            
            <a class="navbar-item "
               href="/categories/Gallery">Gallery</a>
            
            <a class="navbar-item "
               href="/categories/Study">Study</a>
            
            <a class="navbar-item "
               href="/categories/Item">Item</a>
            
            <a class="navbar-item "
               href="/about">About</a>
            
        </div>
        
        <div class="navbar-menu navbar-end">
            
            <a class="navbar-item search" title="搜索" href="javascript:;">
                <i class="fas fa-search"></i>
            </a>
            
            
            <div class="navbar-item is-hoverable has-dropdown is-hidden-mobile is-hidden-tablet-only toc">
                <a class="navbar-item" title="目录">
                    <i class="fa fa-list"></i>
                </a>
                <div class="navbar-dropdown is-right">
                    
                    
                    
                    
                    <a class="navbar-item" href="#Web基础">1&nbsp;&nbsp;<b>Web基础</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#HTTP协议">1.1&nbsp;&nbsp;HTTP协议</a>
                    
                    
                    
                    <a class="navbar-item" href="#编写HTTP-Server">1.2&nbsp;&nbsp;编写HTTP Server</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#Servlet入门">2&nbsp;&nbsp;<b>Servlet入门</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#Servlet开发">3&nbsp;&nbsp;<b>Servlet开发</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#Servlet进阶">4&nbsp;&nbsp;<b>Servlet进阶</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#重定向与转发">4.1&nbsp;&nbsp;重定向与转发</a>
                    
                    
                    
                    <a class="navbar-item" href="#Redirect">4.1.1&nbsp;&nbsp;Redirect</a>
                    
                    
                    
                    <a class="navbar-item" href="#Forward">4.1.2&nbsp;&nbsp;Forward</a>
                    
                    
                    
                    <a class="navbar-item" href="#使用Session和Cookie">4.2&nbsp;&nbsp;使用Session和Cookie</a>
                    
                    
                    
                    <a class="navbar-item" href="#Session">4.2.1&nbsp;&nbsp;Session</a>
                    
                    
                    
                    <a class="navbar-item" href="#Cookie">4.2.2&nbsp;&nbsp;Cookie</a>
                    
                    
                    
                    <a class="navbar-item" href="#JSP开发">4.3&nbsp;&nbsp;JSP开发</a>
                    
                    
                    
                    <a class="navbar-item" href="#JSP高级功能">4.3.1&nbsp;&nbsp;JSP高级功能</a>
                    
                    
                    
                    <a class="navbar-item" href="#JSP-Tag">4.3.2&nbsp;&nbsp;JSP Tag</a>
                    
                    
                    
                    <a class="navbar-item" href="#小结">4.3.3&nbsp;&nbsp;小结</a>
                    
                    
                    
                    <a class="navbar-item" href="#MVC开发">4.4&nbsp;&nbsp;MVC开发</a>
                    
                    
                    
                    <a class="navbar-item" href="#MVC高级开发">4.5&nbsp;&nbsp;MVC高级开发</a>
                    
                    
                    
                    <a class="navbar-item" href="#设计MVC框架">4.5.1&nbsp;&nbsp;设计MVC框架</a>
                    
                    
                    
                    <a class="navbar-item" href="#实现渲染">4.5.2&nbsp;&nbsp;实现渲染</a>
                    
                    
                    
                    <a class="navbar-item" href="#小结-1">4.5.3&nbsp;&nbsp;小结</a>
                    
                    
                    
                    <a class="navbar-item" href="#使用Filter">4.6&nbsp;&nbsp;使用Filter</a>
                    
                    
                    
                    <a class="navbar-item" href="#修改请求">4.6.1&nbsp;&nbsp;修改请求</a>
                    
                    
                    
                    <a class="navbar-item" href="#修改响应">4.6.2&nbsp;&nbsp;修改响应</a>
                    
                    
                    
                    <a class="navbar-item" href="#使用Listener">4.7&nbsp;&nbsp;使用Listener</a>
                    
                    
                    
                    <a class="navbar-item" href="#ServletContext">4.7.1&nbsp;&nbsp;ServletContext</a>
                    
                    
                    
                    <a class="navbar-item" href="#部署">4.8&nbsp;&nbsp;部署</a>
                    
                </div>
            </div>
            
            
            <a class="navbar-item" title="GitHub" target="_blank" rel="noopener" href="https://github.com/wanzixin">
                
                <i class="fab fa-github"></i>
                
            </a>
               
            
        </div>
    </div>
</nav>

    <section class="section">
    <div class="container">
    <article class="article content gallery" itemscope itemprop="blogPost">
    <h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
        
            Web开发
        
    </h1>
    <div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
        <span class="column is-narrow">
            
                <span>6月 16 2021</span>
            
        </span>
        
        <span class="column is-narrow article-category">
            <i class="far fa-folder"></i>
            <a class="article-category-link" href="/zh-cn/categories/Study/">Study</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/">Java</a>
        </span>
        
        
        <span class="column is-narrow">
            
            
            2 小时 读完 (约 19457 字)
        </span>
        
    </div>
    <div class="article-entry is-size-6-mobile" itemprop="articleBody">
    
        <html><head></head><body><p>从本章开始我们就进入到JavaEE的领域。JavaEE并不是一个软件产品，它更多的是一种软件架构和设计思想。我们可以把JavaEE看作是在JavaSE的基础上，开发的一系列基于服务器的组件、API标准和通用架构。<span id="more"></span></p>
<p>JavaEE最核心的部件是基于Servlet标准的Web服务器，开发者编写的应用程序是基于Servlet API并运行在Web服务器内部的。此外，JavaEE还有一系列技术标准：</p>
<ul>
<li>EJB：Enterprise JavaBean，企业级JavaBean，早期经常用于实现应用程序的业务逻辑，现在基本被轻量级框架如Spring所取代</li>
<li>JAAS：Java Authentication and Authorization Service，一个标准的认证和授权服务，常用于企业内部，Web程序通常使用更轻量级的自定义认证</li>
<li>JCA：JavaEE Connector Architecture，用于连接企业内部的EIS系统等</li>
<li>JMS：Java Message Service，用于消息服务</li>
<li>JTA：Java Transaction API，用于分布式事务</li>
<li>JAX-WS：Java API for XML Web Services，用于构建基于XML的Web服务</li>
<li>…</li>
</ul>
<p>目前流行的基于Spring的轻量级JavaEE开发架构，使用最广泛的是Servlet和JMS，以及一系列开源组件。本章我们将详细介绍基于Servlet的Web开发。</p>
<h2 id="Web基础"><a href="#Web基础" class="headerlink" title="Web基础"></a>Web基础</h2><p>今天我们访问网站、使用App时，都是基于Web这种Browser/Server模式，简称BS架构。它的特点是，客户端只需要浏览器，应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器，获取Web页面，并把Web页面展示给用户即可。</p>
<p>因为Web页面是用HTML编写的，而HTML具有强大的表现力，所以Web页面具有极强的交互性。并且，服务端升级后，客户端无需任何部署就可以使用到新版本，因此，BS架构升级非常容易。</p>
<p>对于Browser来说，请求页面的流程如下：</p>
<ol>
<li>与服务器建立TCP连接</li>
<li>发送HTTP请求</li>
<li>接收HTTP相应，然后把网页在浏览器端显示出来</li>
</ol>
<h3 id="HTTP协议"><a href="#HTTP协议" class="headerlink" title="HTTP协议"></a>HTTP协议</h3><p>浏览器发送的HTTP请求如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">GET / HTTP/1.1<br>Host: www.sina.com.cn<br>User-Agent: Mozilla/5.0 xxx<br>Accept: */*<br>Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8<br></code></pre></td></tr></tbody></table></figure>

<p>其中，第一行表示使用<code>GET</code>请求路径为<code>/</code>的资源，并使用<code>HTTP/1.1</code>协议。从第二行开始，每行都是以<code>Header: Value</code>形式表示的HTTP头，比较常用的HTTP Header包括：</p>
<ol>
<li>Host：表示请求的主机名，因为一个服务器上可能运行着多个网站，因此Host表示浏览器正在请求的域名</li>
<li>User-Agent：标识客户端本身，例如Chrome浏览器的标识类似<code>Mozilla/5.0 ... Chrome/79</code>，IE浏览器的标识类似<code>Mozilla/5.0 (Windows NT ...) like Gecko</code></li>
<li>Accept：表示浏览器能接收的资源类型，如<code>text/*</code>，<code>image/*</code>或者<code>*/*</code>表示所有</li>
<li>Accept-Language：表示浏览器偏好的语言，服务器可以根据这个返回不同语言的网页</li>
<li>Accept-Encoding：表示浏览器可以支持的压缩类型，例如<code>gzip</code>, <code>deflate</code>, <code>br</code></li>
</ol>
<p>服务器的响应如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">HTTP/1.1 200 OK<br>Content-Type: text/html<br>Content-Length: 21932<br>Content-Encoding: gzip<br>Cache-Control: max-age=300<br><br>&lt;html&gt;...网页数据...<br></code></pre></td></tr></tbody></table></figure>

<p>服务器响应的第一行总是版本号+空格+数字+空格+文本，数字表示响应码，其中<code>2xx</code>表示成功，3xx表示重定向，<code>4xx</code>表示客户端引发的所务，<code>5xx</code>表示服务端引发的错误。数字是给程序识别，文本是给开发者调试使用的。常见的响应代码有：</p>
<ul>
<li>200 OK：表示成功；</li>
<li>301 Moved Permanently：表示该URL已经永久重定向；</li>
<li>302 Found：表示该URL需要临时重定向；</li>
<li>304 Not Modified：表示该资源没有修改，客户端可以使用本地缓存的版本；</li>
<li>400 Bad Request：表示客户端发送了一个错误的请求，例如参数无效；</li>
<li>401 Unauthorized：表示客户端因为身份未验证而不允许访问该URL；</li>
<li>403 Forbidden：表示服务器因为权限问题拒绝了客户端的请求；</li>
<li>404 Not Found：表示客户端请求了一个不存在的资源；</li>
<li>500 Internal Server Error：表示服务器处理时内部出错，例如因为无法连接数据库；</li>
<li>503 Service Unavailable：表示服务器此刻暂时无法处理请求。</li>
</ul>
<p>从第二行开始，服务器每一行返回一个HTTP头。服务器经常返回的HTTP Header包括：</p>
<ul>
<li>Content-Type：表示该响应内容的类型，例如<code>text/html</code>，<code>image/jpeg</code></li>
<li>Content-Length：表示该响应内容的长度（字节数）</li>
<li>Content-Encoding：表示该响应压缩算法，例如<code>gzip</code></li>
<li>Cache-Control：指示客户端应如何缓存，例如<code>max-age=300</code>表示可以最多缓存300秒</li>
</ul>
<p>HTTP请求和响应都由HTTP Header和HTTP Body构成，其中HTTP Header每行以<code>\r\n</code>。如果遇到连续两个<code>\r\n</code>，那么后面就是HTTP Body。浏览器读取HTTP Body，并根据Header信息中指示的<code>Content-Type</code>，<code>Content-Encoding</code>等解压后显示网页、图像或其他内容。</p>
<p>通常浏览器获取的第一个资源是HTTP网页，在网页中，如果嵌入了JavaScript、CSS、图片、视频等其他资源，浏览器会根据资源的URL再次向服务器请求对应的资源。</p>
<h3 id="编写HTTP-Server"><a href="#编写HTTP-Server" class="headerlink" title="编写HTTP Server"></a>编写HTTP Server</h3><p>一个HTTP Server本质上是一个TCP服务器，我们先用<a href="https://wanzixin.github.io/2021/05/29/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/#TCP%E7%BC%96%E7%A8%8B">TCP编程</a>的知识实现服务器端的框架。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Server</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        ServerSocket ss = <span class="hljs-keyword">new</span> ServerSocket(<span class="hljs-number">8080</span>); <span class="hljs-comment">// 监听指定端口</span><br>        System.out.println(<span class="hljs-string">"server is running..."</span>);<br>        <span class="hljs-keyword">for</span> (;;) {<br>            Socket sock = ss.accept();<br>            System.out.println(<span class="hljs-string">"connected from "</span> + sock.getRemoteSocketAddress());<br>            Thread t = <span class="hljs-keyword">new</span> Handler(sock);<br>            t.start();<br>        }<br>    }<br>}<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Handler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Thread</span> </span>{<br>    Socket sock;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Handler</span><span class="hljs-params">(Socket sock)</span> </span>{<br>        <span class="hljs-keyword">this</span>.sock = sock;<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">this</span>.sock.getInputStream()) {<br>            <span class="hljs-keyword">try</span> (OutputStream output = <span class="hljs-keyword">this</span>.sock.getOutputStream()) {<br>                handle(input, output);<br>            }<br>        } <span class="hljs-keyword">catch</span> (Exception e) {<br>            <span class="hljs-keyword">try</span> {<br>                <span class="hljs-keyword">this</span>.sock.close();<br>            } <span class="hljs-keyword">catch</span> (IOException ioe) {<br>            }<br>            System.out.println(<span class="hljs-string">"client disconnected."</span>);<br>        }<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handle</span><span class="hljs-params">(InputStream input, OutputStream output)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">var</span> reader = <span class="hljs-keyword">new</span> BufferedReader(<span class="hljs-keyword">new</span> InputStreamReader(input, StandardCharsets.UTF_8));<br>        <span class="hljs-keyword">var</span> writer = <span class="hljs-keyword">new</span> BufferedWriter(<span class="hljs-keyword">new</span> OutputStreamWriter(output, StandardCharsets.UTF_8));<br>        <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 处理HTTP请求</span><br>        System.out.println(<span class="hljs-string">"Process new http request..."</span>);<br>    <span class="hljs-keyword">var</span> reader = <span class="hljs-keyword">new</span> BufferedReader(<span class="hljs-keyword">new</span> InputStreamReader(input, StandardCharsets.UTF_8));<br>    <span class="hljs-keyword">var</span> writer = <span class="hljs-keyword">new</span> BufferedWriter(<span class="hljs-keyword">new</span> OutputStreamWriter(output, StandardCharsets.UTF_8));<br>    <span class="hljs-comment">// 读取HTTP请求:</span><br>    <span class="hljs-keyword">boolean</span> requestOk = <span class="hljs-keyword">false</span>;<br>    String first = reader.readLine();<br>    <span class="hljs-keyword">if</span> (first.startsWith(<span class="hljs-string">"GET / HTTP/1."</span>)) {<br>        requestOk = <span class="hljs-keyword">true</span>;<br>    }<br>    <span class="hljs-keyword">for</span> (;;) {<br>        String header = reader.readLine();<br>        <span class="hljs-keyword">if</span> (header.isEmpty()) { <span class="hljs-comment">// 读取到空行时, HTTP Header读取完毕</span><br>            <span class="hljs-keyword">break</span>;<br>        }<br>        System.out.println(header);<br>    }<br>    System.out.println(requestOk ? <span class="hljs-string">"Response OK"</span> : <span class="hljs-string">"Response Error"</span>);<br>    <span class="hljs-keyword">if</span> (!requestOk) {<br>        <span class="hljs-comment">// 发送错误响应:</span><br>        writer.write(<span class="hljs-string">"HTTP/1.0 404 Not Found\r\n"</span>);<br>        writer.write(<span class="hljs-string">"Content-Length: 0\r\n"</span>);<br>        writer.write(<span class="hljs-string">"\r\n"</span>);<br>        writer.flush();<br>    } <span class="hljs-keyword">else</span> {<br>        <span class="hljs-comment">// 发送成功响应:</span><br>        String data = <span class="hljs-string">"&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello, world!&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;"</span>;<br>        <span class="hljs-keyword">int</span> length = data.getBytes(StandardCharsets.UTF_8).length;<br>        writer.write(<span class="hljs-string">"HTTP/1.0 200 OK\r\n"</span>);<br>        writer.write(<span class="hljs-string">"Connection: close\r\n"</span>);<br>        writer.write(<span class="hljs-string">"Content-Type: text/html\r\n"</span>);<br>        writer.write(<span class="hljs-string">"Content-Length: "</span> + length + <span class="hljs-string">"\r\n"</span>);<br>        writer.write(<span class="hljs-string">"\r\n"</span>); <span class="hljs-comment">// 空行标识Header和Body的分隔</span><br>        writer.write(data);<br>        writer.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这里的核心代码是，先读取HTTP请求，这里我们只处理<code>GET /</code>的请求。当读取到空行时，表示已读到连续两个<code>\r\n</code>，说明请求结束，可以发送响应。发送响应的时候，首先发送响应代码<code>HTTP/1.0 200 OK</code>表示一个成功的200响应，使用<code>HTTP/1.0</code>协议，然后，依次发送Header，发送完Header后，再发送一个空行标识Header结束，紧接着发送HTTP Body，在浏览器输入<code>http://localhost:8080/</code>就可以看到响应页面。</p>
<blockquote>
<p>HTTP目前有多个版本，<code>1.0</code>是早期版本，浏览器每次建立TCP连接后，只发送一个HTTP请求并接收一个HTTP响应，然后就关闭TCP连接。由于创建TCP连接本身就需要消耗一定的时间，因此，HTTP 1.1允许浏览器和服务器在同一个TCP连接上反复发送、接收多个HTTP请求和响应，这样就大大提高了传输效率。</p>
<p>我们注意到HTTP协议是一个请求-响应协议，它总是发送一个请求，然后接收一个响应。能不能一次性发送多个请求，然后再接收多个响应呢？HTTP 2.0可以支持浏览器同时发出多个请求，但每个请求需要唯一标识，服务器可以不按请求的顺序返回多个响应，由浏览器自己把收到的响应和请求对应起来。可见，HTTP 2.0进一步提高了传输效率，因为浏览器发出一个请求后，不必等待响应，就可以继续发下一个请求。</p>
<p>HTTP 3.0为了进一步提高速度，将抛弃TCP协议，改为使用无需创建连接的UDP协议，目前HTTP 3.0仍然处于实验阶段。</p>
</blockquote>
<h2 id="Servlet入门"><a href="#Servlet入门" class="headerlink" title="Servlet入门"></a>Servlet入门</h2><p>在上一节中我们看到，编写HTTP服务器其实是比较简单的，只需要先编写基于多线程的TCP服务，然后在一个TCP连接中去读HTTP请求，发送HTTP响应即可。但是，要编写一个完善的HTTP服务器，以HTTP/1.1为例，需要考虑：</p>
<ul>
<li>识别正确和错误的HTTP请求；</li>
<li>识别正确和错误的HTTP头；</li>
<li>复用TCP连接；</li>
<li>复用线程；</li>
<li>IO异常处理；</li>
<li>…</li>
</ul>
<p>这些基础的工作需要耗费大量的时间，并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的HTML页面，就不得不编写上千行底层代码，那就无法做到高效而可靠的开发。</p>
<p>因此，在JavaEE平台上，处理TCP连接，解析HTTP协议这些底层工作统统交给现成的Web服务器去做，我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的，JavaEE提供了Servlet API，我们使用Servlet API编写自己的Servlet来处理HTTP请求，Web服务器实现Servlet API接口，实现底层功能：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                 ┌───────────┐<br>                 │My Servlet │<br>                 ├───────────┤<br>                 │Servlet API│<br>┌───────┐  HTTP  ├───────────┤<br>│Browser│&lt;──────&gt;│Web Server │<br>└───────┘        └───────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>我们来实现一个最简单的Servlet：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// WebServlet注解表示这是一个Servlet，并映射到地址/:</span><br><span class="hljs-meta">@WebServlet(urlPatterns = "/")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 设置响应类型:</span><br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        <span class="hljs-comment">// 获取输出流:</span><br>        PrintWriter pw = resp.getWriter();<br>        <span class="hljs-comment">// 写入响应:</span><br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Hello, world!&lt;/h1&gt;"</span>);<br>        <span class="hljs-comment">// 最后不要忘记flush强制输出:</span><br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>一个Servlet总是继承自<code>HttpServlet</code>，然后覆写<code>doGet()</code>和<code>doPost()</code>方法。注意到doGet()方法传入了<code>HttpServletRequest</code>和<code>HttpServletResponse</code>，分别表示HTTP请求和响应。我们使用Servlet API时，并不与底层TCP交互。也不需要解析HTTP协议，因为<code>HttpServletRequest</code>和<code>HttpServletResponse</code>都已经封装好了请求和响应。以发送响应为例，我们只需要设置正确的响应类型，然后获取<code>PrintWriter</code>，写入响应即可。</p>
<p>那么Servlet API是谁提供的呢？</p>
<p>Servlet API是一个jar包，我们需要通过Maven来引入它，才能正常编译：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">packaging</span>&gt;</span>war<span class="hljs-tag">&lt;/<span class="hljs-name">packaging</span>&gt;</span><br><br><span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>javax.servlet<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>javax.servlet-api<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>4.0.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">scope</span>&gt;</span>provided<span class="hljs-tag">&lt;/<span class="hljs-name">scope</span>&gt;</span><br>  <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>注意到这个<code>pom.xml</code>与之前讲到的普通Java程序有点不同，打包类型是不<code>jar</code>而是<code>war</code>，表示Java Web Application Archive。注意到<code>&lt;scope&gt;</code>指定为<code>provided</code>，表示编译时使用，但不会打包到<code>.war</code>文件中，因为运行期Web服务器本身已经提供了Servlet API相关的jar包。</p>
<p>我们还需要在工程目录下创建一个web.xml描述文件，放到<code>src/main/webapp/WEB-INF</code>目录下（固定目录结构，不要修改路径，注意大小写）。文件内容可以固定如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java">&lt;!DOCTYPE web-app PUBLIC<br> <span class="hljs-string">"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"</span><br> <span class="hljs-string">"http://java.sun.com/dtd/web-app_2_3.dtd"</span>&gt;<br>&lt;web-app&gt;<br>  &lt;display-name&gt;Archetype Created Web Application&lt;/display-name&gt;<br>&lt;/web-app&gt;<br></code></pre></td></tr></tbody></table></figure>

<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs ascii">web-servlet-hello<br>├── pom.xml<br>└── src<br>    └── main<br>        ├── java<br>        │   └── com<br>        │       └── itranswarp<br>        │           └── learnjava<br>        │               └── servlet<br>        │                   └── HelloServlet.java<br>        ├── resources<br>        └── webapp<br>            └── WEB-INF<br>                └── web.xml<br></code></pre></td></tr></tbody></table></figure>

<p>运行Maven命令<code>mvn clean package</code>，在target目录下得到一个hello.war文件，这个文件就是我们编译打包后的Web应用程序。</p>
<p>普通的Java程序是通过启动JVM，然后执行<code>main()</code>方法开始运行。但是Web应用程序有所不同，我们无法直接运行<code>war</code>文件，必须先启动Web服务器，再由Web服务器加载我们编写的<code>HelloServlet</code>，这样就可以让<code>HelloServlet</code>处理浏览器发送的请求。</p>
<p>因此，我们首先要找一个支持Servlet API的Web服务器。常用的服务器有：</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://tomcat.apache.org/">Tomcat</a>：由Apache开发的开源免费服务器；</li>
<li><a target="_blank" rel="noopener" href="https://www.eclipse.org/jetty/">Jetty</a>：由Eclipse开发的开源免费服务器；</li>
<li><a target="_blank" rel="noopener" href="https://javaee.github.io/glassfish/">GlassFish</a>：一个开源的全功能JavaEE服务器。</li>
</ul>
<p>无论使用哪个服务器，只要它支持Servlet API 4.0（因为我们引入的Servlet版本是4.0），我们的war包都可以在上面运行。这里我们选择使用最广泛的开源免费的Tomcat服务器。</p>
<p>要运行我们的<code>hello.war</code>，首先要下载Tomcat服务器，解压后，把<code>hello.war</code>复制到Tomcat的<code>webapps</code>目录下，然后切换到<code>bin</code>目录，执行<code>startup.sh</code>或<code>startup.bat</code>启动Tomcat服务器。</p>
<p>在浏览器输入<code>http://localhost:8080/hello/</code>即可看到<code>HelloServlet</code>的输出。细心的童鞋可能会问，为啥路径是<code>/hello/</code>而不是<code>/</code>？因为一个Web服务器允许同时运行多个Web App，而我们的Web App叫<code>hello</code>，因此，第一级目录<code>/hello</code>表示Web App的名字，后面的<code>/</code>才是我们在<code>HelloServlet</code>中映射的路径。</p>
<p>那能不能直接使用<code>/</code>而不是<code>/hello/</code>？毕竟<code>/</code>比较简洁。</p>
<p>答案是肯定的。先关闭Tomcat（执行<code>shutdown.sh</code>或<code>shutdown.bat</code>），然后删除Tomcat的webapps目录下的所有文件夹和文件，最后把我们的<code>hello.war</code>复制过来，改名为<code>ROOT.war</code>，文件名为<code>ROOT</code>的应用程序将作为默认应用，启动后直接访问<code>http://localhost:8080/</code>即可。</p>
<p>实际上，类似Tomcat这样的服务器也是Java编写的，启动Tomcat服务器实际上是启动Java虚拟机，执行Tomcat的<code>main()</code>方法，然后由Tomcat负责加载我们的<code>.war</code>文件，并创建一个<code>HelloServlet</code>实例，最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是<code>/</code>（假定部署文件为ROOT.war），就转发到<code>HelloServlet</code>并传入<code>HttpServletRequest</code>和<code>HttpServletResponse</code>两个对象。</p>
<p>因为我们编写的Servlet并不是直接运行，而是由Web服务器加载后创建实例运行，所以，类似Tomcat这样的Web服务器也称为Servlet容器。</p>
<p>在Servlet容器中运行的Servlet具有如下特点：</p>
<ul>
<li>无法在代码中直接通过new创建Servlet实例，必须由Servlet容器自动创建Servlet实例</li>
<li>Servlet容器只会给每个Servlet类创建唯一实例</li>
<li>Servlet容器会使用多线程执行<code>doGet()</code>或<code>doPost()</code>方法</li>
</ul>
<p>复习一下Java多线程的内容，我们可以得出结论：</p>
<ul>
<li>在Servlet中定义的实例变量会被多个线程同时访问，要注意线程安全</li>
<li><code>HttpServletRequest</code>和<code>HttpServletResponse</code>实例是由Servlet容器传入的局部变量，它们只能被当前线程访问，不存在多个线程访问的问题</li>
<li>在<code>doGet()</code>或<code>doPost()</code>方法中，如果使用了<code>ThreadLocal</code>，但没有清理，那么它的状态很可能会影响到下次的某个请求，因为Servlet容器很可能用线程池实现线程复用</li>
</ul>
<p>因此，正确编写Servlet，要清晰理解Java的多线程模型，需要同步访问的必须同步。</p>
<h2 id="Servlet开发"><a href="#Servlet开发" class="headerlink" title="Servlet开发"></a>Servlet开发</h2><p>在上一节中我们看到，一个完整的Web应用程序开发流程如下：</p>
<ol>
<li>编写Servlet</li>
<li>打包为war文件</li>
<li>复制到Tomcat的webapps目录下</li>
<li>启动Tomcat</li>
</ol>
<p>这个过程有点繁琐，比如我们想在IDE中断点调试，还需要打开Tomcat的远程调试窗口并连接上去。因此，我们需要一种简单可靠，能直接在IDE中启动并调试webapp的方法。我们先来看看Tomcat的启动流程：</p>
<ol>
<li>启动JVM并执行Tomcat的main()方法</li>
<li>加载war并初始化Servlet</li>
<li>正常服务</li>
</ol>
<p>启动Tomcat无非就是设置好classpath并执行Tomcat某个jar包的<code>main()</code>方法，我们完全可以把Tomcat的jar包全部引入进来，然后自己编写<code>main()</code>方法，让它先启动Tomcat，再加载我们的webapp就行。</p>
<p>我们新建一个<code>web-servlet-embedded</code>工程，编写<code>pom.xml</code>如下：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">project</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://maven.apache.org/POM/4.0.0"</span></span><br><span class="hljs-tag">    <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="hljs-tag">    <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">modelVersion</span>&gt;</span>4.0.0<span class="hljs-tag">&lt;/<span class="hljs-name">modelVersion</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>com.itranswarp.learnjava<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>web-servlet-embedded<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>1.0-SNAPSHOT<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">packaging</span>&gt;</span>war<span class="hljs-tag">&lt;/<span class="hljs-name">packaging</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">properties</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">project.build.sourceEncoding</span>&gt;</span>UTF-8<span class="hljs-tag">&lt;/<span class="hljs-name">project.build.sourceEncoding</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">project.reporting.outputEncoding</span>&gt;</span>UTF-8<span class="hljs-tag">&lt;/<span class="hljs-name">project.reporting.outputEncoding</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">maven.compiler.source</span>&gt;</span>11<span class="hljs-tag">&lt;/<span class="hljs-name">maven.compiler.source</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">maven.compiler.target</span>&gt;</span>11<span class="hljs-tag">&lt;/<span class="hljs-name">maven.compiler.target</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">java.version</span>&gt;</span>11<span class="hljs-tag">&lt;/<span class="hljs-name">java.version</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">tomcat.version</span>&gt;</span>9.0.26<span class="hljs-tag">&lt;/<span class="hljs-name">tomcat.version</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">properties</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.tomcat.embed<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>tomcat-embed-core<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${tomcat.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">scope</span>&gt;</span>provided<span class="hljs-tag">&lt;/<span class="hljs-name">scope</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.tomcat.embed<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>tomcat-embed-jasper<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${tomcat.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">scope</span>&gt;</span>provided<span class="hljs-tag">&lt;/<span class="hljs-name">scope</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">project</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>其中，<code>&lt;packaging&gt;</code>类型仍然为war，引入依赖<code>tomcat-embed-core</code>和<code>tomcat-embed-jasper</code>。不必再引入Servlet API，因为引入的Tomcat依赖启动后自动引入了Servlet API，我们正常编写Servlet如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        String name = req.getParameter(<span class="hljs-string">"name"</span>);<br>        <span class="hljs-keyword">if</span> (name == <span class="hljs-keyword">null</span>) {<br>            name = <span class="hljs-string">"world"</span>;<br>        }<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Hello, "</span> + name + <span class="hljs-string">"!&lt;/h1&gt;"</span>);<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>然后，我们编写一个main()方法，启动Tomcat服务器：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        <span class="hljs-comment">// 启动Tomcat:</span><br>        Tomcat tomcat = <span class="hljs-keyword">new</span> Tomcat();<br>        tomcat.setPort(Integer.getInteger(<span class="hljs-string">"port"</span>, <span class="hljs-number">8080</span>));<br>        tomcat.getConnector();<br>        <span class="hljs-comment">// 创建webapp:</span><br>        Context ctx = tomcat.addWebapp(<span class="hljs-string">""</span>, <span class="hljs-keyword">new</span> File(<span class="hljs-string">"src/main/webapp"</span>).getAbsolutePath());<br>        WebResourceRoot resources = <span class="hljs-keyword">new</span> StandardRoot(ctx);<br>        resources.addPreResources(<br>                <span class="hljs-keyword">new</span> DirResourceSet(resources, <span class="hljs-string">"/WEB-INF/classes"</span>, <span class="hljs-keyword">new</span> File(<span class="hljs-string">"target/classes"</span>).getAbsolutePath(), <span class="hljs-string">"/"</span>));<br>        ctx.setResources(resources);<br>        tomcat.start();<br>        tomcat.getServer().await();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这样，我们运行<code>main()</code>方法，即可启动嵌入式Tomcat服务器，然后，通过预设的<code>tomcat.addWebapp("", new File("src/main/webapp"))</code>，Tomcat会自动加载当前工程为根webapp，可直接在浏览器访问<a target="_blank" rel="noopener" href="http://localhost:8080/">http://localhost:8080/</a>。</p>
<p>通过<code>main()</code>方法启动Tomcat服务器并加载我们自己的webapp有如下好处：</p>
<ol>
<li>启动简单，无需下载Tomcat或安装任何IDE插件</li>
<li>调试方便，可在IDE中使用断点调试</li>
<li>使用Maven创建war包后，也可以正常部署到独立的Tomcar应用服务器</li>
</ol>
<blockquote>
<p>对SpringBoot有所了解的童鞋可能知道，SpringBoot也支持在<code>main()</code>方法中一行代码直接启动Tomcat，并且还能方便地更换成Jetty等其他服务器。它的启动方式和我们介绍的是基本一样的，后续涉及到SpringBoot的部分我们还会详细讲解。</p>
</blockquote>
<p>开发Servlet时，推荐使用main()方法启动嵌入式Tomcat服务器并加载当前工程的webapp，便于开发调试，且不影响打包部署，能提升开发效率。</p>
<h2 id="Servlet进阶"><a href="#Servlet进阶" class="headerlink" title="Servlet进阶"></a>Servlet进阶</h2><p>一个Web App是由一个或多个Servlet组成，每个Servlet通过注解说明自己能处理的路径。比如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/hello")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        ...<br>    }<br>  ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述<code>HelloServlet</code>能处理<code>/hello</code>这个路径的请求。早期的Servlet需要在<code>web.xml</code>中配置映射路径，但最新Servlet版本只需要通过注解就可以完成映射。</p>
<p>浏览器发送请求时，还会有请求方法（HTTP Method）：即GET、POST、PUT等不同类型的请求，因此要处理GET请求必须覆写<code>doGet()</code>方法。类似地，要处理POST请求，就需要覆写<code>doPost()</code>方法。如果没有覆写<code>doPost()</code>方法，那么HelloServlet能不能处理POST/hello请求呢？</p>
<p>我们查看HttpServlet的<code>doPost()</code>：它会直接返回405或400错误。因此，如果一个Servlet映射到<code>/hello</code>，那么这个路径下的所有请求方法都会由这个Servlet来处理，至于能不能返回200成功响应，要看有没有覆写对应的请求方法。</p>
<p>一个WebApp完全可以有多个Servlet，分别映射不同的路径。浏览器发送的HTTP请求总是由Web Server先接收，然后根据Servlet配置的映射，不同的路径转发到不同的Servlet。这种根据路径转发的功能一般称为Dispatch。映射到/的Servlet比较特殊，它会接收所有未匹配的路径。</p>
<p>一、<code>HttpServletRequest</code>封装了一个HTTP请求，通过HttpServletRequest的接口几乎可以拿到HTTP请求的全部信息。</p>
<ul>
<li>getMethod()：返回请求方法，例如，<code>"GET"</code>，<code>"POST"</code></li>
<li>getRequestURI()：返回请求路径，但不包括请求参数，例如，<code>"/hello"</code></li>
<li>getQueryString()：返回请求参数，例如，<code>"name=Bob&amp;a=1&amp;b=2"</code></li>
<li>getParameter(name)：返回请求参数，GET请求从URL读取参数，POST请求从Body中读取参数</li>
<li>getContentType()：获取请求Body的类型，例如，<code>"application/x-www-form-urlencoded"</code></li>
<li>getContextPath()：获取当前Webapp挂载的路径，对于ROOT来说，总是返回空字符串<code>""</code></li>
<li>getCookies()：返回请求携带的所有Cookie</li>
<li>getHeader(name)：获取指定的Header，对Header名称不区分大小写</li>
<li>getHeaderNames()：返回所有Header名称</li>
<li>getInputStream()：如果该请求带有HTTP Body，该方法将打开一个输入流用于读取Body</li>
<li>getReader()：和getInputStream()类似，但打开的是Reader</li>
<li>getRemoteAddr()：返回客户端的IP地址</li>
<li>getScheme()：返回协议类型，例如，<code>"http"</code>，<code>"https"</code></li>
</ul>
<p>此外，<code>HttpServletRequest</code>还有两个方法：<code>setAttribute()</code>和<code>getAttribute()</code>，可以给当前<code>HttpServletRequest</code>对象附加多个Key-Value，相当于把<code>HttpServletRequest</code>当作一个<code>Map&lt;String, Object&gt;</code>使用。调用<code>HttpServletRequest</code>的方法时，注意务必阅读接口方法的文档说明，因为有的方法会返回<code>null</code>。</p>
<p>二、<code>HttpServletResponse</code>封装了一个HTTP响应。由于HTTP响应必须先发送Header，再发送Body，所以，操作<code>HttpServletResponse</code>对象时，必须先调用设置Header的方法，最后调用发送Body的方法。</p>
<p>常用的设置Header的方法有：</p>
<ul>
<li>setStatus(sc)：设置响应代码，默认是<code>200</code></li>
<li>setContentType(type)：设置Body的类型，例如，<code>"text/html"</code></li>
<li>setCharacterEncoding(charset)：设置字符编码，例如，<code>"UTF-8"</code></li>
<li>setHeader(name, value)：设置一个Header的值</li>
<li>addCookie(cookie)：给响应添加一个Cookie</li>
<li>addHeader(name, value)：给响应添加一个Header，因为HTTP协议允许有多个相同的Header</li>
</ul>
<p>写入响应时，需要通过<code>getOutputStream()</code>获取写入流，或者通过<code>getWriter()</code>获取字符流，二者只能获取其中一个。写入响应前，无需设置<code>setContentLength()</code>，因为底层服务器会根据写入的字节数自动设置，如果写入的数据量很小，实际上会写入缓冲区，如果写入的数据量很大，服务器会自动采用Chunked编码让浏览器能识别数据结束符而不需要设置Content-Length。</p>
<p>但是写完后调用<code>flush()</code>是必须的，因为大部分Web服务器都基于HTTP/1.1协议，会复用TCP连接。如果没有调用<code>flush()</code>会导致缓冲区的内容无法及时发送到客户端。此外，写入完毕后千万不要调用<code>close()</code>，因为会有复用TCP连接，如果关闭写入流，将关闭TCP连接，使得Web服务器无法复用此连接。</p>
<p>有了HttpServletRequest和HttpServletResponse这两个高级接口，我们就不需要直接处理HTTP协议，注意到具体的实现类是由各服务器提供的，而我们编写的Web应用程序只关心接口方法，不需要关心具体的实现子类。</p>
<p>三、一个Servlet类在服务器中只有一个实例，但对于每个HTTP请求，Web服务器会使用多线程执行请求。因此，一个Servlet的<code>doGet()</code>、<code>doPost()</code>等处理请求是多线程并发执行的。如果Servlet中定义了字段，要注意多线程并发访问的问题。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-keyword">private</span> Map&lt;String, String&gt; map = <span class="hljs-keyword">new</span> ConcurrentHashMap&lt;&gt;();<br><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 注意读写map字段是多线程并发的:</span><br>        <span class="hljs-keyword">this</span>.map.put(key, value);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>对于每个请求，Web服务器会创建唯一的<code>HttpServletRequest</code>和<code>HttpServletResponse</code>实例，因此，<code>HttpServletRequest</code>和<code>HttpServletResponse</code>实例只有在当前处理线程中有效，它们总是局部变量，不存在多线程共享的问题。</p>
<h3 id="重定向与转发"><a href="#重定向与转发" class="headerlink" title="重定向与转发"></a>重定向与转发</h3><h4 id="Redirect"><a href="#Redirect" class="headerlink" title="Redirect"></a>Redirect</h4><p>重定向是指浏览器请求一个URL时，浏览器返会一个重定向指令，告诉浏览器地址已经变了，请使用新的URL发送新的请求。</p>
<p>例如，我们已经编写了一个能处理<code>/hello</code>的<code>HelloServlet</code>，如果收到的路径为<code>/hi</code>，希望能重定向到<code>/hello</code>，可以再编写一个<code>RedirectServlet</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/hi")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RedirectServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 构造重定向的路径:</span><br>        String name = req.getParameter(<span class="hljs-string">"name"</span>);<br>        String redirectToUrl = <span class="hljs-string">"/hello"</span> + (name == <span class="hljs-keyword">null</span> ? <span class="hljs-string">""</span> : <span class="hljs-string">"?name="</span> + name);<br>        <span class="hljs-comment">// 发送重定向响应:</span><br>        resp.sendRedirect(redirectToUrl);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>使用浏览器发送<code>GET /hi</code>请求，<code>RedirectServlet</code>将处理此请求，<code>RedirectServlet</code>内部又发送了重定向响应，因此浏览器会收到如下响应：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">HTTP/1.1 302 Found<br>Location: /hello<br></code></pre></td></tr></tbody></table></figure>

<p>当浏览器收到302响应时，它会立刻根据<code>Location</code>的提示发送一个新的<code>GET /hello</code>请求，这个过程就是重定向。观察Chrome浏览器的网络请求，可以看到两次HTTP请求。</p>
<p>重定向有两种：一种是302响应，称为临时重定向；一种是301响应，称为永久重定向。两者的区别是，如果服务器发301永久重定向响应，浏览器会缓存<code>/hi</code>到<code>/hello</code>这个重定向的关联，下次请求<code>/hi</code>的时候，浏览器就直接发送<code>/hello</code>请求。</p>
<p>重定向有什么作用？重定向的目的是当Web应用升级时，如果请求路径发生了变化，可以将原来的路径重定向到新路径，避免浏览器请求原路径找不到资源。</p>
<p><code>HttpServletResponse</code>提供了快捷的<code>redirect()</code>方法实现302重定向。如果要实现301永久重定向，可以这么写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java">resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); <span class="hljs-comment">// 301</span><br>resp.setHeader(<span class="hljs-string">"Location"</span>, <span class="hljs-string">"/hello"</span>);<br></code></pre></td></tr></tbody></table></figure>

<h4 id="Forward"><a href="#Forward" class="headerlink" title="Forward"></a>Forward</h4><p>Forward是指内部转发。当一个Servlet处理请求时，它可以自己决定不处理，而是转发给另一个Servlet处理。</p>
<p>例如，我们编写一个能处理<code>/hello</code>的<code>HelloServlet</code>，继续编写一个能处理<code>/morning</code>的<code>ForwardServlet</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/morning")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ForwardServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        req.getRequestDispatcher(<span class="hljs-string">"/hello"</span>).forward(req, resp);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p><code>ForwardServlet</code>在收到请求后，它并不自己发送响应，而是把请求和响应都转发给路径为<code>/hello</code>的Servlet。后续请求的处理实际上是由<code>HelloServlet</code>完成的。这种处理方式称为转发（Forward）。</p>
<p>转发和重定向的区别在于，转发是在Web服务器内部完成的，对浏览器来说它只发送了一个HTTP请求，浏览器并不知道请求在Web服务器内部做了一次转发。</p>
<h3 id="使用Session和Cookie"><a href="#使用Session和Cookie" class="headerlink" title="使用Session和Cookie"></a>使用Session和Cookie</h3><p>在Web应用程序中，我们经常需要跟踪用户身份。当一个用户登录成功后，如果用户继续访问其他页面，Web程序如何才能识别用户身份？而HTTP协议是无状态的，即Web应用程序无法区分收到的两个HTTP请求是否是同一个浏览器发出的。为了跟踪用户状态，服务器可以向浏览器分配一个唯一ID，并以Cookie的形式发送到浏览器，浏览器在后续访问时总附带此Cookie，这样，服务器就可以识别用户身份。</p>
<h4 id="Session"><a href="#Session" class="headerlink" title="Session"></a>Session</h4><p>我们把这种基于唯一ID识别用户身份的机制称为Session。每个用户第一次访问服务器后，会自动获得一个Session ID。如果用户一段时间内没有访问服务器，那么Session就会自动失效，下次即使带着上次分配的Session ID访问，服务器也会认为是一个新用户，会分配新的Session ID。</p>
<p>JavaEE的Servlet机制内建了对Session的支持。我们以登录为例，当一个用户登录成功后，我们就可以把这个用户的名字放入一个<code>HttpSession</code>对象，以便后续访问其他页面时，能直接从<code>HttpSession</code>取出用户名。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/signin")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignInServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-comment">// 模拟一个数据库:</span><br>    <span class="hljs-keyword">private</span> Map&lt;String, String&gt; users = Map.of(<span class="hljs-string">"bob"</span>, <span class="hljs-string">"bob123"</span>, <span class="hljs-string">"alice"</span>, <span class="hljs-string">"alice123"</span>, <span class="hljs-string">"tom"</span>, <span class="hljs-string">"tomcat"</span>);<br><br>    <span class="hljs-comment">// GET请求时显示登录页:</span><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Sign In&lt;/h1&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;form action=\"/signin\" method=\"post\"&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;p&gt;Username: &lt;input name=\"username\"&gt;&lt;/p&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;p&gt;Password: &lt;input name=\"password\" type=\"password\"&gt;&lt;/p&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;p&gt;&lt;button type=\"submit\"&gt;Sign In&lt;/button&gt; &lt;a href=\"/\"&gt;Cancel&lt;/a&gt;&lt;/p&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;/form&gt;"</span>);<br>        pw.flush();<br>    }<br><br>    <span class="hljs-comment">// POST请求时处理用户登录:</span><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doPost</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        String name = req.getParameter(<span class="hljs-string">"username"</span>);<br>        String password = req.getParameter(<span class="hljs-string">"password"</span>);<br>        String expectedPassword = users.get(name.toLowerCase());<br>        <span class="hljs-keyword">if</span> (expectedPassword != <span class="hljs-keyword">null</span> &amp;&amp; expectedPassword.equals(password)) {<br>            <span class="hljs-comment">// 登录成功:</span><br>            req.getSession().setAttribute(<span class="hljs-string">"user"</span>, name);<br>            resp.sendRedirect(<span class="hljs-string">"/"</span>);<br>        } <span class="hljs-keyword">else</span> {<br>            resp.sendError(HttpServletResponse.SC_FORBIDDEN);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在<code>IndexServlet</code>中，可以从<code>HttpSession</code>取出用户名：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IndexServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 从HttpSession获取当前用户名:</span><br>        String user = (String) req.getSession().getAttribute(<span class="hljs-string">"user"</span>);<br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        resp.setCharacterEncoding(<span class="hljs-string">"UTF-8"</span>);<br>        resp.setHeader(<span class="hljs-string">"X-Powered-By"</span>, <span class="hljs-string">"JavaEE Servlet"</span>);<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Welcome, "</span> + (user != <span class="hljs-keyword">null</span> ? user : <span class="hljs-string">"Guest"</span>) + <span class="hljs-string">"&lt;/h1&gt;"</span>);<br>        <span class="hljs-keyword">if</span> (user == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 未登录，显示登录链接:</span><br>            pw.write(<span class="hljs-string">"&lt;p&gt;&lt;a href=\"/signin\"&gt;Sign In&lt;/a&gt;&lt;/p&gt;"</span>);<br>        } <span class="hljs-keyword">else</span> {<br>            <span class="hljs-comment">// 已登录，显示登出链接:</span><br>            pw.write(<span class="hljs-string">"&lt;p&gt;&lt;a href=\"/signout\"&gt;Sign Out&lt;/a&gt;&lt;/p&gt;"</span>);<br>        }<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果用户已登录，可以通过访问<code>/signout</code>登出。登出逻辑就是从<code>HttpSession</code>中移除用户相关信息：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/signout")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignOutServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 从HttpSession移除用户名:</span><br>        req.getSession().removeAttribute(<span class="hljs-string">"user"</span>);<br>        resp.sendRedirect(<span class="hljs-string">"/"</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>对于Web应用程序来说，我们总是通过<code>HttpSession</code>这个高级接口访问当前Session。要深入理解Session原理，可以任务Web服务器在内存中自动维护了一个ID到<code>HttpSession</code>的映射表。而服务器识别Session的关键是依靠一个名为<code>JSESSIONID</code>的Cookie。在Servlet中第一次调用<code>req.getSession()</code>时，Servlet容器自动创建一个Session ID，然后通过一个名为<code>JSESSIONID</code>的Cookie发送给浏览器。</p>
<p>这里要注意的几点是：</p>
<ul>
<li><code>JSESSIONID</code>是由Servlet容器自动创建的，目的是维护一个浏览器回话，它和我们的登录逻辑没有关系</li>
<li>登录和登出的业务逻辑是我们自己根据HttpSession是否存在一个<code>“user”</code>的Key判断的，登出后，Session ID并不会改变</li>
<li>即使没有登录功能，仍然可以使用HttpSession追踪用户，例如，放入一些用户配置信息</li>
</ul>
<p>除了使用Cookie机制实现Session之外，还可以通过隐藏表单、URL末尾附加ID来追踪Session。这些机制很少使用，最常用的Session机制仍然是Cookie。</p>
<p>使用Session时，由于服务器把所有用户的Session都存储在内存中，如果遇到内存不足的情况，就需要把部分不活动的Session序列化到磁盘上，这会大大降低服务器的运行效率。因此，放入Session的对象要小，通常我们放入一个简单的<code>User</code>对象就够了。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> id; <span class="hljs-comment">// 唯一标识</span><br>    <span class="hljs-keyword">public</span> String email;<br>    <span class="hljs-keyword">public</span> String name;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在使用多台服务器构建集群时，使用Session会遇到一些额外的问题。通常，多台服务器集群使用反向代理作为网站入口。</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                                     ┌────────────┐<br>                                ┌───&gt;│Web Server 1│<br>                                │    └────────────┘<br>┌───────┐     ┌─────────────┐   │    ┌────────────┐<br>│Browser│────&gt;│Reverse Proxy│───┼───&gt;│Web Server 2│<br>└───────┘     └─────────────┘   │    └────────────┘<br>                                │    ┌────────────┐<br>                                └───&gt;│Web Server 3│<br>                                     └────────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>如果多台Web Server采用无状态集群，那么反向代理总是以轮询的方式将请求依此转发给每台Web Server，这回造成一个用户在Web Server1存储的Session信息，在Web Server2和3上并不存在。即从Web Server1登录后，如果后续请求被转发到Web Server2和3，那么依然会是无登录状态。</p>
<p>要解决这个问题，方案一是在所有Web Server之间进行Session复制，但这样会严重消耗网络带宽。并且，每台Web Server的内存都存储了所有用户的Session，内存使用率很低。</p>
<p>另一个方案是采用粘滞会话（Sticky Session）机制，即反向代理在转发请求时，总是根据JSESSIONID的值判断，相同的JSESSIONID总是转发到固定的Web Server，但这需要反向代理的支持。</p>
<p>无论采用哪种方案，使用Session机制会使得Web Server的集群很难扩展，因此，Session适用于中小型Web应用程序。对于大型Web应用程序来说，通常需要避免采用Session机制。</p>
<h4 id="Cookie"><a href="#Cookie" class="headerlink" title="Cookie"></a>Cookie</h4><p>实际上，Servlet提供的<code>HttpSession</code>本质上就是通过一个名为<code>JSESSIONID</code>的Cookie来跟踪用户会话的。除了这个名称外，其他名称的Cookie我们可以任意使用。</p>
<p>如果我们想要设置一个Cookie，例如记录用户选择的语言，可以编写一个<code>LanguageServlet</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/pref")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LanguageServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Set&lt;String&gt; LANGUAGES = Set.of(<span class="hljs-string">"en"</span>, <span class="hljs-string">"zh"</span>);<br><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        String lang = req.getParameter(<span class="hljs-string">"lang"</span>);<br>        <span class="hljs-keyword">if</span> (LANGUAGES.contains(lang)) {<br>            <span class="hljs-comment">// 创建一个新的Cookie:</span><br>            Cookie cookie = <span class="hljs-keyword">new</span> Cookie(<span class="hljs-string">"lang"</span>, lang);<br>            <span class="hljs-comment">// 该Cookie生效的路径范围:</span><br>            cookie.setPath(<span class="hljs-string">"/"</span>);<br>            <span class="hljs-comment">// 该Cookie有效期:</span><br>            cookie.setMaxAge(<span class="hljs-number">8640000</span>); <span class="hljs-comment">// 8640000秒=100天</span><br>            <span class="hljs-comment">// 将该Cookie添加到响应:</span><br>            resp.addCookie(cookie);<br>        }<br>        resp.sendRedirect(<span class="hljs-string">"/"</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>创建一个Cookie时，除了指定名称和值以外，通常需要设置<code>setPath("/")</code>，浏览器根据此前缀决定是否发送Cookie。如果一个Cookie调用了<code>setPath("/user/")</code>，那么浏览器只有在请求以<code>/user/</code>开头的路径时才会附加此Cookie。通过<code>setMaxAge()</code>设置Cookie的有效期，单位为秒，最后通过<code>resp.addCookie()</code>把它添加到响应。如果访问的是https网页，还需要调用<code>setSecure(true)</code>，否则浏览器不会发送该Cookie。</p>
<p>因此，务必注意：浏览器在请求某个URL时，是否携带指定的Cookie，取决于Cookie是否满足以下所有要求：</p>
<ul>
<li>URL前缀是设置Cookie时的Path；</li>
<li>Cookie在有效期内；</li>
<li>Cookie设置了secure时必须以https访问。</li>
</ul>
<p>我们可以在浏览器看到服务器发送的Cookie。</p>
<p>如果我们要读取Cookie，例如，在<code>IndexServlet</code>中，读取名为<code>lang</code>的Cookie以获取用户设置的语言，可以写一个方法如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">private</span> String <span class="hljs-title">parseLanguageFromCookie</span><span class="hljs-params">(HttpServletRequest req)</span> </span>{<br>    <span class="hljs-comment">// 获取请求附带的所有Cookie:</span><br>    Cookie[] cookies = req.getCookies();<br>    <span class="hljs-comment">// 如果获取到Cookie:</span><br>    <span class="hljs-keyword">if</span> (cookies != <span class="hljs-keyword">null</span>) {<br>        <span class="hljs-comment">// 循环每个Cookie:</span><br>        <span class="hljs-keyword">for</span> (Cookie cookie : cookies) {<br>            <span class="hljs-comment">// 如果Cookie名称为lang:</span><br>            <span class="hljs-keyword">if</span> (cookie.getName().equals(<span class="hljs-string">"lang"</span>)) {<br>                <span class="hljs-comment">// 返回Cookie的值:</span><br>                <span class="hljs-keyword">return</span> cookie.getValue();<br>            }<br>        }<br>    }<br>    <span class="hljs-comment">// 返回默认值:</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-string">"en"</span>;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>可见，读取Cookie主要依靠遍历<code>HttpServletRequest</code>附带的所有Cookie。</p>
<h3 id="JSP开发"><a href="#JSP开发" class="headerlink" title="JSP开发"></a>JSP开发</h3><p>从前面的章节中可以看到，Servlet就是一个能处理HTTP请求、发送HTTP响应的小程序，而发送响应无非就是获取<code>PrintWriter</code>，然后输出HTML。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java">PrintWriter pw = resp.getWriter();<br>pw.write(<span class="hljs-string">"&lt;html&gt;"</span>);<br>pw.write(<span class="hljs-string">"&lt;body&gt;"</span>);<br>pw.write(<span class="hljs-string">"&lt;h1&gt;Welcome, "</span> + name + <span class="hljs-string">"!&lt;/h1&gt;"</span>);<br>pw.write(<span class="hljs-string">"&lt;/body&gt;"</span>);<br>pw.write(<span class="hljs-string">"&lt;/html&gt;"</span>);<br>pw.flush();<br></code></pre></td></tr></tbody></table></figure>

<p>只不过，用<code>PrintWriter</code>输出HTML比较痛苦，因为不但要正确编写HTML，还需要插入各种变量。如果想在Servlet中输出一个类似新浪首页的HTML，写对HTML基本不可能。那么有没有更简单的输出HTML的方法？</p>
<p>有！</p>
<p>我们可以使用JSP，JSP是Java Server Pages的缩写，它的文件必须放到<code>/src/main/webapp</code>下，文件名必须以<code>.jsp</code>结尾，整个文件与HTML无太大差别。但需要插入变量，或者动态输出的地方，使用特殊指令<code>&lt;%...%&gt;</code>。我们来编写一个<code>hello.jsp</code>，内容如下：</p>
<figure class="highlight jsp hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs jsp">&lt;html&gt;<br>&lt;head&gt;<br>    &lt;title&gt;Hello World - JSP&lt;/title&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>    &lt;%-- JSP Comment --%&gt;<br>    &lt;h1&gt;Hello World!&lt;/h1&gt;<br>    &lt;p&gt;<br>    &lt;%<br>         out.println(<span class="hljs-string">"Your IP address is "</span>);<br>    %&gt;<br>    &lt;span style=<span class="hljs-string">"color:red"</span>&gt;<br>        &lt;%= request.getRemoteAddr() %&gt;<br>    &lt;/span&gt;<br>    &lt;/p&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;<br></code></pre></td></tr></tbody></table></figure>

<ul>
<li>包含在<code>&lt;%--</code>和<code>--%&gt;</code>之间的是JSP的注释，它们会被完全忽略</li>
<li>包含在<code>&lt;%</code>和<code>%&gt;</code>之间的是Java代码，可以编写任意Java代码</li>
<li>如果使用<code>&lt;%= xxx %&gt;</code>则可以快捷输出一个变量的值</li>
</ul>
<p>JSP页面内置了几个变量，这几个变量可以直接使用：</p>
<ul>
<li>out：表示HttpServletResponse的PrintWriter</li>
<li>session：表示当前HttpSession对象</li>
<li>request：表示HttpServletRequest对象</li>
</ul>
<p>访问JSP时，直接指定完整路径，例如，<code>http://localhost:8080/hello.jsp</code>。</p>
<p>JSP和Servlet有什么区别？其实它们没有区别，因为JSP在执行前会被编译为一个Servlet。在Tomcat的临时目录下，可以找到一个<code>hello_jsp.java</code>的源文件，这个文件就是Tomcat把JSP自动转换为Servlet源码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> org.apache.jsp;<br><span class="hljs-keyword">import</span> ...<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">hello_jsp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">org</span>.<span class="hljs-title">apache</span>.<span class="hljs-title">jasper</span>.<span class="hljs-title">runtime</span>.<span class="hljs-title">HttpJspBase</span></span><br><span class="hljs-class">    <span class="hljs-keyword">implements</span> <span class="hljs-title">org</span>.<span class="hljs-title">apache</span>.<span class="hljs-title">jasper</span>.<span class="hljs-title">runtime</span>.<span class="hljs-title">JspSourceDependent</span>,</span><br><span class="hljs-class">               <span class="hljs-title">org</span>.<span class="hljs-title">apache</span>.<span class="hljs-title">jasper</span>.<span class="hljs-title">runtime</span>.<span class="hljs-title">JspSourceImports</span> </span>{<br><br>    ...<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">_jspService</span><span class="hljs-params">(<span class="hljs-keyword">final</span> javax.servlet.http.HttpServletRequest request, <span class="hljs-keyword">final</span> javax.servlet.http.HttpServletResponse response)</span></span><br><span class="hljs-function">        <span class="hljs-keyword">throws</span> java.io.IOException, javax.servlet.ServletException </span>{<br>        ...<br>        out.write(<span class="hljs-string">"&lt;html&gt;\n"</span>);<br>        out.write(<span class="hljs-string">"&lt;head&gt;\n"</span>);<br>        out.write(<span class="hljs-string">"    &lt;title&gt;Hello World - JSP&lt;/title&gt;\n"</span>);<br>        out.write(<span class="hljs-string">"&lt;/head&gt;\n"</span>);<br>        out.write(<span class="hljs-string">"&lt;body&gt;\n"</span>);<br>        ...<br>    }<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>可见JSP本质上就是一个Servlet，只不过无需配置映射路径，Web Server会根据路径查找对应的<code>.jsp</code>文件，如果找到了，就自动编译成Servlet再执行。在服务器运行过程中，如果修改了JSP的内容，那么服务器会自动重新编译。</p>
<h4 id="JSP高级功能"><a href="#JSP高级功能" class="headerlink" title="JSP高级功能"></a>JSP高级功能</h4><p>JSP的指令非常复杂，除了<code>&lt;% ... %&gt;</code>外，JSP页面本身可以通过<code>page</code>指令引入Java类：</p>
<figure class="highlight jsp hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs jsp">&lt;%@ page <span class="hljs-keyword">import</span>=<span class="hljs-string">"java.io.*"</span> %&gt;<br>&lt;%@ page <span class="hljs-keyword">import</span>=<span class="hljs-string">"java.util.*"</span> %&gt;<br></code></pre></td></tr></tbody></table></figure>

<p>这样后续的Java代码才能引用简单类名而不是完整类名。</p>
<p>使用<code>include</code>指令可以引入另一个JSP文件：</p>
<figure class="highlight jsp hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs jsp">&lt;html&gt;<br>&lt;body&gt;<br>    &lt;%@ include file=<span class="hljs-string">"header.jsp"</span>%&gt;<br>    &lt;h1&gt;Index Page&lt;/h1&gt;<br>    &lt;%@ include file=<span class="hljs-string">"footer.jsp"</span>%&gt;<br>&lt;/body&gt;<br></code></pre></td></tr></tbody></table></figure>

<h4 id="JSP-Tag"><a href="#JSP-Tag" class="headerlink" title="JSP Tag"></a>JSP Tag</h4><p>JSP还允许自定义输出的tag，例如：</p>
<figure class="highlight jsp hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs jsp">&lt;c:out value = <span class="hljs-string">"${sessionScope.user.name}"</span>/&gt;<br></code></pre></td></tr></tbody></table></figure>

<p>JSP Tag需要正确引入taglib的jar包，并且还需要正确声明，使用起来非常复杂，对于页面开发来说，<strong>不推荐</strong>使用JSP Tag，因为我们后续会介绍更简单的模板引擎，这里我们不再介绍如何使用taglib。</p>
<h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>JSP是一种在HTML中嵌入动态输出的文件，它和Servlet正好相反，Servlet是在Java代码中嵌入输出的HTML。JSP可以引用并使用JSP Tag，但由于其语法复杂，不推荐使用。JSP目前已很少使用，我们只需要了解其基本用法即可。</p>
<h3 id="MVC开发"><a href="#MVC开发" class="headerlink" title="MVC开发"></a>MVC开发</h3><p>通过前面的章节可以看到：</p>
<ul>
<li>Servlet适合编写Java代码，实现各种复杂的业务逻辑，但不适合输出复杂的HTML</li>
<li>JSP适合编写HTML，并在其中插入动态内容，但不适合编写复杂的Java代码</li>
</ul>
<p>能否将两者结合起来，发挥各自的优点？答案是肯定的，这就是MVC，MVC模式是一种分离业务逻辑和显示逻辑的设计模式，广泛应用在Web和桌面应用程序。我们来看一个例子。</p>
<p>假设我们已经编写了几个JavaBean：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> id;<br>    <span class="hljs-keyword">public</span> String name;<br>    <span class="hljs-keyword">public</span> School school;<br>}<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">School</span> </span>{<br>    <span class="hljs-keyword">public</span> String name;<br>    <span class="hljs-keyword">public</span> String address;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在<code>UserServlet</code>中，我们可以从数据库读取<code>User</code>、<code>School</code>等信息，然后，把读取到的JavaBean先放到HttpServletRequest中，再通过<code>forward()</code>传给<code>user.jsp</code>处理：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/user")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 假装从数据库读取:</span><br>        School school = <span class="hljs-keyword">new</span> School(<span class="hljs-string">"No.1 Middle School"</span>, <span class="hljs-string">"101 South Street"</span>);<br>        User user = <span class="hljs-keyword">new</span> User(<span class="hljs-number">123</span>, <span class="hljs-string">"Bob"</span>, school);<br>        <span class="hljs-comment">// 放入Request中:</span><br>        req.setAttribute(<span class="hljs-string">"user"</span>, user);<br>        <span class="hljs-comment">// forward给user.jsp:</span><br>        req.getRequestDispatcher(<span class="hljs-string">"/WEB-INF/user.jsp"</span>).forward(req, resp);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在<code>user.jsp</code>中，我们只负责展示相关JavaBean的信息，不需要编写访问数据库等复杂逻辑：</p>
<figure class="highlight jsp hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs jsp">&lt;%@ page <span class="hljs-keyword">import</span>=<span class="hljs-string">"com.itranswarp.learnjava.bean.*"</span>%&gt;<br>&lt;%<br>    User user = (User) request.getAttribute(<span class="hljs-string">"user"</span>);<br>%&gt;<br>&lt;html&gt;<br>&lt;head&gt;<br>    &lt;title&gt;Hello World - JSP&lt;/title&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>    &lt;h1&gt;Hello &lt;%= user.name %&gt;!&lt;/h1&gt;<br>    &lt;p&gt;School Name:<br>    &lt;span style=<span class="hljs-string">"color:red"</span>&gt;<br>        &lt;%= user.school.name %&gt;<br>    &lt;/span&gt;<br>    &lt;/p&gt;<br>    &lt;p&gt;School Address:<br>    &lt;span style=<span class="hljs-string">"color:red"</span>&gt;<br>        &lt;%= user.school.address %&gt;<br>    &lt;/span&gt;<br>    &lt;/p&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;<br></code></pre></td></tr></tbody></table></figure>

<p>请注意几点：</p>
<ul>
<li>需要展示的<code>User</code>被放入<code>HttpServletRequest</code>中以便传递给JSP，因为一个请求对应一个<code>HttpServletRequest</code>，我们也无需清理它，处理完该请求后<code>HttpServletRequest</code>实例将被丢弃</li>
<li>把<code>user.jsp</code>放到<code>/WEB-INF/</code>目录下，是因为<code>WEB-INF</code>是一个特殊目录，Web Server会阻止浏览器对<code>WEB-INF</code>目录下任何资源的访问，这样就防止用户通过<code>/user.jsp</code>路径直接访问到JSP页面</li>
<li>JSP页面首先从<code>request</code>变量获取<code>User</code>实例，然后在页面中直接输出，此处未考虑HTML的转义问题，有潜在安全风险</li>
</ul>
<p>我们在浏览器访问<a target="_blank" rel="noopener" href="http://localhost:8080/user">http://localhost:8080/user</a>，请求首先由<code>UserServlet</code>处理，然后交给<code>user.jsp</code>渲染。</p>
<p>我们把<code>UserServlet</code>看作业务逻辑处理，把<code>User</code>看作模型，把<code>user.jsp</code>看作渲染，这种设计模式通常称为MVC：Model-View-Controller。即<code>User</code>作为模型Model，<code>user.jsp</code>作为视图View，<code>UserServlet</code>作为控制器Controller。整个MVC架构如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                   ┌───────────────────────┐<br>             ┌────&gt;│Controller: UserServlet│<br>             │     └───────────────────────┘<br>             │                 │<br>┌───────┐    │           ┌─────┴─────┐<br>│Browser│────┘           │Model: User│<br>│       │&lt;───┐           └─────┬─────┘<br>└───────┘    │                 │<br>             │                 ▼<br>             │     ┌───────────────────────┐<br>             └─────│    View: user.jsp     │<br>                   └───────────────────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>使用MVC模式的好处是，Controller专注于业务处理，它的处理结果是Model。Model可以是一个JavaBean，也可以是一个包含多个对象的Map，Controller只负责把Model传递给View，View只负责把Model给渲染出来。这样，三者职责明确且开发更简单。但它还不够简洁和灵活，后续我们会介绍更简单的Spring MVC开发。</p>
<h3 id="MVC高级开发"><a href="#MVC高级开发" class="headerlink" title="MVC高级开发"></a>MVC高级开发</h3><p>通过结合Servlet和JSP的MVC模式，我们可以发挥二者各自的优点。但是，直接把MVC搭在Servlet和JSP上还是不太好，因为：</p>
<ul>
<li>Servlet提供的接口仍然偏底层，需要实现Servlet调用相关的接口</li>
<li>JSP对页面开发不太友好，更好的替代品是模板引擎</li>
<li>业务逻辑最好用纯粹的Java类实现，而不是强迫继承自Servlet</li>
</ul>
<p>能不能通过普通的Java类实现MVC的Controller？类似下面的代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{<br>    <span class="hljs-meta">@GetMapping("/signin")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">signin</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br><br>    <span class="hljs-meta">@PostMapping("/signin")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">doSignin</span><span class="hljs-params">(SignInBean bean)</span> </span>{<br>        ...<br>    }<br><br>    <span class="hljs-meta">@GetMapping("/signout")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">signout</span><span class="hljs-params">(HttpSession session)</span> </span>{<br>        ...<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上面的这个Java类每个方法都对应一个GET或POST请求，方法返回值是<code>ModelAndView</code>，它包含一个View的路径以及一个Model，这样，再由MVC框架处理后返回给浏览器。</p>
<p>如果是GET请求，我们希望MVC框架能直接把URL参数按方法参数对应起来然后传入：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@GetMapping("/hello")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">hello</span><span class="hljs-params">(String name)</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果是POST请求，我们希望MVC框架能直接把Post参数变成一个JavaBean后通过方法参数传入：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@PostMapping("/signin")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">doSignin</span><span class="hljs-params">(SignInBean bean)</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>为了增加灵活性，如果Controller方法在处理请求时需要访问<code>HttpServletRequest</code>、<code>HttpServletResponse</code>、<code>HttpSession</code>这些实例时，只要方法参数有定义，就可以自动传入：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@GetMapping("/signout")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">signout</span><span class="hljs-params">(HttpSession session)</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<h4 id="设计MVC框架"><a href="#设计MVC框架" class="headerlink" title="设计MVC框架"></a>设计MVC框架</h4><p>如何设计一个MVC框架？在上文中，我们已经定义了上层代码编写Controller的一切接口信息，并且并不需要实现特定接口，只需返回<code>ModelAndView</code>对象，该对象包含一个View和Model。实际上View就是模板的路径，而Model可以用一个<code>Map&lt;String, Object&gt;</code>表示，因此<code>ModelAndView</code>的定义非常简单：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ModelAndView</span> </span>{<br>    Map&lt;String, Object&gt; model;<br>    String view;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>比较复杂的是我们需要在MVC框架创建一个接收所有请求的<code>Servlet</code>，通常我们把它命名为<code>DispatcherServlet</code>，它总是映射到<code>/</code>。然后，根据不同的Controller的方法定义的<code>@Get</code>和<code>@Post</code>的Path决定调用哪个方法。最后，获得方法返回的<code>ModelAndView</code>后，渲染模板，写入<code>HttpServletResponse</code>，即完成了整个MVC的处理。</p>
<p>这个MVC的框架如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs ascii">   HTTP Request    ┌─────────────────┐<br>──────────────────&gt;│DispatcherServlet│<br>                   └─────────────────┘<br>                            │<br>               ┌────────────┼────────────┐<br>               ▼            ▼            ▼<br>         ┌───────────┐┌───────────┐┌───────────┐<br>         │Controller1││Controller2││Controller3│<br>         └───────────┘└───────────┘└───────────┘<br>               │            │            │<br>               └────────────┼────────────┘<br>                            ▼<br>   HTTP Response ┌────────────────────┐<br>&lt;────────────────│render(ModelAndView)│<br>                 └────────────────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>其中，<code>DispatcherServlet</code>以及如何渲染均由MVC框架实现，在MVC框架之上只需要编写每一个Controller。</p>
<p>我们来看看如何编写最复杂的<code>DispatcherServlet</code>。首先，我们需要存储请求路径到某个具体方法的映射：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DispatcherServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-keyword">private</span> Map&lt;String, GetDispatcher&gt; getMappings = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();<br>    <span class="hljs-keyword">private</span> Map&lt;String, PostDispatcher&gt; postMappings = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>处理一个GET请求是通过GetDispather对象完成的，它需要如下信息：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GetDispatcher</span> </span>{<br>    Object instance; <span class="hljs-comment">// Controller实例</span><br>    Method method; <span class="hljs-comment">// Controller方法</span><br>    String[] parameterNames; <span class="hljs-comment">// 方法参数名称</span><br>    Class&lt;?&gt;[] parameterClasses; <span class="hljs-comment">// 方法参数类型</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>有了以上信息，就可以定义<code>invoke()</code>来处理真正的请求：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GetDispatcher</span> </span>{<br>    ...<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">invoke</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response)</span> </span>{<br>        Object[] arguments = <span class="hljs-keyword">new</span> Object[parameterClasses.length];<br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; parameterClasses.length; i++) {<br>            String parameterName = parameterNames[i];<br>            Class&lt;?&gt; parameterClass = parameterClasses[i];<br>            <span class="hljs-keyword">if</span> (parameterClass == HttpServletRequest.class) {<br>                arguments[i] = request;<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == HttpServletResponse.class) {<br>                arguments[i] = response;<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == HttpSession.class) {<br>                arguments[i] = request.getSession();<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == <span class="hljs-keyword">int</span>.class) {<br>                arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, <span class="hljs-string">"0"</span>));<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == <span class="hljs-keyword">long</span>.class) {<br>                arguments[i] = Long.valueOf(getOrDefault(request, parameterName, <span class="hljs-string">"0"</span>));<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == <span class="hljs-keyword">boolean</span>.class) {<br>                arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, <span class="hljs-string">"false"</span>));<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == String.class) {<br>                arguments[i] = getOrDefault(request, parameterName, <span class="hljs-string">""</span>);<br>            } <span class="hljs-keyword">else</span> {<br>                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"Missing handler for type: "</span> + parameterClass);<br>            }<br>        }<br>        <span class="hljs-keyword">return</span> (ModelAndView) <span class="hljs-keyword">this</span>.method.invoke(<span class="hljs-keyword">this</span>.instance, arguments);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> String <span class="hljs-title">getOrDefault</span><span class="hljs-params">(HttpServletRequest request, String name, String defaultValue)</span> </span>{<br>        String s = request.getParameter(name);<br>        <span class="hljs-keyword">return</span> s == <span class="hljs-keyword">null</span> ? defaultValue : s;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述代码比较繁琐，但逻辑非常简单，即通过构造某个方法需要的所有参数列表，使用反射调用该方法返回结果。类似地，<code>PostDispatcher</code>需要如下信息：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PostDispatcher</span> </span>{<br>    Object instance; <span class="hljs-comment">// Controller实例</span><br>    Method method; <span class="hljs-comment">// Controller方法</span><br>    Class&lt;?&gt;[] parameterClasses; <span class="hljs-comment">// 方法参数类型</span><br>    ObjectMapper objectMapper; <span class="hljs-comment">// JSON映射</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>和GET请求不同，POST请求严格来说不能有URL参数，所有数据都应当从Post Body中读取。这里我们为了简化处理，<em>只支持</em>JSON格式的POST请求，这样，把Post数据转化为JavaBean就非常容易。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PostDispatcher</span> </span>{<br>    ...<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">invoke</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response)</span> </span>{<br>        Object[] arguments = <span class="hljs-keyword">new</span> Object[parameterClasses.length];<br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; parameterClasses.length; i++) {<br>            Class&lt;?&gt; parameterClass = parameterClasses[i];<br>            <span class="hljs-keyword">if</span> (parameterClass == HttpServletRequest.class) {<br>                arguments[i] = request;<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == HttpServletResponse.class) {<br>                arguments[i] = response;<br>            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (parameterClass == HttpSession.class) {<br>                arguments[i] = request.getSession();<br>            } <span class="hljs-keyword">else</span> {<br>                <span class="hljs-comment">// 读取JSON并解析为JavaBean:</span><br>                BufferedReader reader = request.getReader();<br>                arguments[i] = <span class="hljs-keyword">this</span>.objectMapper.readValue(reader, parameterClass);<br>            }<br>        }<br>        <span class="hljs-keyword">return</span> (ModelAndView) <span class="hljs-keyword">this</span>.method.invoke(instance, arguments);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>最后，我们来实现整个<code>DispatcherServlet</code>的处理流程，以<code>doGet()</code>为例。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DispatcherServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    ...<br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        resp.setCharacterEncoding(<span class="hljs-string">"UTF-8"</span>);<br>        String path = req.getRequestURI().substring(req.getContextPath().length());<br>        <span class="hljs-comment">// 根据路径查找GetDispatcher:</span><br>        GetDispatcher dispatcher = <span class="hljs-keyword">this</span>.getMappings.get(path);<br>        <span class="hljs-keyword">if</span> (dispatcher == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 未找到返回404:</span><br>            resp.sendError(<span class="hljs-number">404</span>);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 调用Controller方法获得返回值:</span><br>        ModelAndView mv = dispatcher.invoke(req, resp);<br>        <span class="hljs-comment">// 允许返回null:</span><br>        <span class="hljs-keyword">if</span> (mv == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 允许返回`redirect:`开头的view表示重定向:</span><br>        <span class="hljs-keyword">if</span> (mv.view.startsWith(<span class="hljs-string">"redirect:"</span>)) {<br>            resp.sendRedirect(mv.view.substring(<span class="hljs-number">9</span>));<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 将模板引擎渲染的内容写入响应:</span><br>        PrintWriter pw = resp.getWriter();<br>        <span class="hljs-keyword">this</span>.viewEngine.render(mv, pw);<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这里有几个小改进：</p>
<ul>
<li>允许Controller方法返回null，表示内部已自行处理完毕</li>
<li>允许Controller方法返回以<code>redirect:</code>开头的view名称，表示一个重定向</li>
</ul>
<p>这样使得上层代码编写更灵活，例如，一个显示用户资料的请求可以这样写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@GetMapping("/user/profile")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">profile</span><span class="hljs-params">(HttpServletResponse response, HttpSession session)</span> </span>{<br>    User user = (User) session.getAttribute(<span class="hljs-string">"user"</span>);<br>    <span class="hljs-keyword">if</span> (user == <span class="hljs-keyword">null</span>) {<br>        <span class="hljs-comment">// 未登录，跳转到登录页:</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ModelAndView(<span class="hljs-string">"redirect:/signin"</span>);<br>    }<br>    <span class="hljs-keyword">if</span> (!user.isManager()) {<br>        <span class="hljs-comment">// 权限不够，返回403:</span><br>        response.sendError(<span class="hljs-number">403</span>);<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;<br>    }<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ModelAndView(<span class="hljs-string">"/profile.html"</span>, Map.of(<span class="hljs-string">"user"</span>, user));<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>最后一步是在<code>DispatcherServlet</code>的<code>init()</code>方法中初始化所有Get和Post的映射，以及用于渲染的模板引擎：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DispatcherServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-keyword">private</span> Map&lt;String, GetDispatcher&gt; getMappings = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();<br>    <span class="hljs-keyword">private</span> Map&lt;String, PostDispatcher&gt; postMappings = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();<br>    <span class="hljs-keyword">private</span> ViewEngine viewEngine;<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> ServletException </span>{<br>        <span class="hljs-keyword">this</span>.getMappings = scanGetInControllers();<br>        <span class="hljs-keyword">this</span>.postMappings = scanPostInControllers();<br>        <span class="hljs-keyword">this</span>.viewEngine = <span class="hljs-keyword">new</span> ViewEngine(getServletContext());<br>    }<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如何扫描所有Controller以获取所有标记有<code>@GetMapping</code>和<code>@PostMapping</code>的方法？当然是使用反射了。虽然代码比较繁琐，但我们相信各位童鞋可以轻松实现。</p>
<p>这样，整个MVC框架就搭建完毕。</p>
<h4 id="实现渲染"><a href="#实现渲染" class="headerlink" title="实现渲染"></a>实现渲染</h4><p>有的童鞋对如何使用模板引擎进行渲染有疑问，即如何实现上述的<code>ViewEngine</code>？其实<code>ViewEngine</code>非常简单，只需要实现一个简单的<code>render()</code>方法：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ViewEngine</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">render</span><span class="hljs-params">(ModelAndView mv, Writer writer)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        String view = mv.view;<br>        Map&lt;String, Object&gt; model = mv.model;<br>        <span class="hljs-comment">// 根据view找到模板文件:</span><br>        Template template = getTemplateByPath(view);<br>        <span class="hljs-comment">// 渲染并写入Writer:</span><br>        template.write(writer, model);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>Java有很多开源的模板引擎，常用的有Thymeleaf，FreeMarker，Velocity。它们的用法都大同小异，这里我们推荐一个使用<a target="_blank" rel="noopener" href="https://palletsprojects.com/p/jinja/">Jinja</a>语法的模板引擎<a target="_blank" rel="noopener" href="https://pebbletemplates.io/">Pebble</a>，它的特点是语法简单，支持模板继承，编写出来的模板类似：</p>
<figure class="highlight html hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span><br>  {% for user in users %}<br>    <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ user.url }}"</span>&gt;</span>{{ user.username }}<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span><br>  {% endfor %}<br>  <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>即变量用<code>{{ xxx }}</code>表示，控制语句用<code>{% xxx %}</code>表示。</p>
<p>使用Pebble渲染只需要如下几行代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ViewEngine</span> </span>{<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> PebbleEngine engine;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ViewEngine</span><span class="hljs-params">(ServletContext servletContext)</span> </span>{<br>        <span class="hljs-comment">// 定义一个ServletLoader用于加载模板:</span><br>        ServletLoader loader = <span class="hljs-keyword">new</span> ServletLoader(servletContext);<br>        <span class="hljs-comment">// 模板编码:</span><br>        loader.setCharset(<span class="hljs-string">"UTF-8"</span>);<br>        <span class="hljs-comment">// 模板前缀，这里默认模板必须放在`/WEB-INF/templates`目录:</span><br>        loader.setPrefix(<span class="hljs-string">"/WEB-INF/templates"</span>);<br>        <span class="hljs-comment">// 模板后缀:</span><br>        loader.setSuffix(<span class="hljs-string">""</span>);<br>        <span class="hljs-comment">// 创建Pebble实例:</span><br>        <span class="hljs-keyword">this</span>.engine = <span class="hljs-keyword">new</span> PebbleEngine.Builder()<br>            .autoEscaping(<span class="hljs-keyword">true</span>) <span class="hljs-comment">// 默认打开HTML字符转义，防止XSS攻击</span><br>            .cacheActive(<span class="hljs-keyword">false</span>) <span class="hljs-comment">// 禁用缓存使得每次修改模板可以立刻看到效果</span><br>            .loader(loader).build();<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">render</span><span class="hljs-params">(ModelAndView mv, Writer writer)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-comment">// 查找模板:</span><br>        PebbleTemplate template = <span class="hljs-keyword">this</span>.engine.getTemplate(mv.view);<br>        <span class="hljs-comment">// 渲染:</span><br>        template.evaluate(writer, mv.model);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>最后我们来看看整个工程的结构：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs ascii">web-mvc<br>├── pom.xml<br>└── src<br>    └── main<br>        ├── java<br>        │   └── com<br>        │       └── itranswarp<br>        │           └── learnjava<br>        │               ├── Main.java<br>        │               ├── bean<br>        │               │   ├── SignInBean.java<br>        │               │   └── User.java<br>        │               ├── controller<br>        │               │   ├── IndexController.java<br>        │               │   └── UserController.java<br>        │               └── framework<br>        │                   ├── DispatcherServlet.java<br>        │                   ├── FileServlet.java<br>        │                   ├── GetMapping.java<br>        │                   ├── ModelAndView.java<br>        │                   ├── PostMapping.java<br>        │                   └── ViewEngine.java<br>        └── webapp<br>            ├── WEB-INF<br>            │   ├── templates<br>            │   │   ├── _base.html<br>            │   │   ├── hello.html<br>            │   │   ├── index.html<br>            │   │   ├── profile.html<br>            │   │   └── signin.html<br>            │   └── web.xml<br>            └── static<br>                ├── css<br>                │   └── bootstrap.css<br>                └── js<br>                    ├── bootstrap.js<br>                    └── jquery.js<br></code></pre></td></tr></tbody></table></figure>

<p>其中，<code>framework</code>包是MVC的框架，完全可以单独编译后作为一个Maven依赖引入，<code>controller</code>包才是我们需要编写的业务逻辑。</p>
<p>我们还硬性规定模板必须放在<code>webapp/WEB-INF/templates</code>目录下，静态文件必须放在<code>webapp/static</code>目录下，因此，为了便于开发，我们还顺带实现一个<code>FileServlet</code>来处理静态文件：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FileServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 读取当前请求路径:</span><br>        ServletContext ctx = req.getServletContext();<br>        <span class="hljs-comment">// RequestURI包含ContextPath,需要去掉:</span><br>        String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());<br>        <span class="hljs-comment">// 获取真实文件路径:</span><br>        String filepath = ctx.getRealPath(urlPath);<br>        <span class="hljs-keyword">if</span> (filepath == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 无法获取到路径:</span><br>            resp.sendError(HttpServletResponse.SC_NOT_FOUND);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        Path path = Paths.get(filepath);<br>        <span class="hljs-keyword">if</span> (!path.toFile().isFile()) {<br>            <span class="hljs-comment">// 文件不存在:</span><br>            resp.sendError(HttpServletResponse.SC_NOT_FOUND);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 根据文件名猜测Content-Type:</span><br>        String mime = Files.probeContentType(path);<br>        <span class="hljs-keyword">if</span> (mime == <span class="hljs-keyword">null</span>) {<br>            mime = <span class="hljs-string">"application/octet-stream"</span>;<br>        }<br>        resp.setContentType(mime);<br>        <span class="hljs-comment">// 读取文件并写入Response:</span><br>        OutputStream output = resp.getOutputStream();<br>        <span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">new</span> BufferedInputStream(<span class="hljs-keyword">new</span> FileInputStream(filepath))) {<br>            input.transferTo(output);<br>        }<br>        output.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>运行代码，在浏览器中输入URL<code>http://localhost:8080/hello?name=Bob</code>即可看到渲染出来的页面。</p>
<p>有些用过Spring MVC的童鞋会发现，本节实现的这个MVC框架，上层代码使用的公共类如<code>GetMapping</code>、<code>PostMapping</code>和<code>ModelAndView</code>都和Spring MVC非常类似。实际上，我们这个MVC框架主要参考就是Spring MVC，通过实现一个“简化版”MVC，可以掌握Java Web MVC开发的核心思想与原理，对将来直接使用Spring MVC是非常有帮助的。</p>
<h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h4><p>一个MVC框架是基于Servlet基础抽象出更高级的接口，使得上层基于MVC框架的开发可以不涉及Servlet相关的<code>HttpServletRequest</code>等接口，处理多个请求更加灵活，并且可以使用任意模板引擎，不必使用JSP。</p>
<h3 id="使用Filter"><a href="#使用Filter" class="headerlink" title="使用Filter"></a>使用Filter</h3><p>在一个比较复杂的Web应用程序中，通常都有很多URL映射，对应的，也会有多个Servlet来处理URL。我们考察这样一个论坛应用程序：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs ascii">            ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐<br>               /             ┌──────────────┐<br>            │ ┌─────────────&gt;│ IndexServlet │ │<br>              │              └──────────────┘<br>            │ │/signin       ┌──────────────┐ │<br>              ├─────────────&gt;│SignInServlet │<br>            │ │              └──────────────┘ │<br>              │/signout      ┌──────────────┐<br>┌───────┐   │ ├─────────────&gt;│SignOutServlet│ │<br>│Browser├─────┤              └──────────────┘<br>└───────┘   │ │/user/profile ┌──────────────┐ │<br>              ├─────────────&gt;│ProfileServlet│<br>            │ │              └──────────────┘ │<br>              │/user/post    ┌──────────────┐<br>            │ ├─────────────&gt;│ PostServlet  │ │<br>              │              └──────────────┘<br>            │ │/user/reply   ┌──────────────┐ │<br>              └─────────────&gt;│ ReplyServlet │<br>            │                └──────────────┘ │<br>             ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─<br></code></pre></td></tr></tbody></table></figure>

<p>各个Servlet设计功能如下：</p>
<ul>
<li>IndexServlet：浏览帖子</li>
<li>SignInServlet：登录</li>
<li>SignOutServlet：登出</li>
<li>ProfileServlet：修改用户资料</li>
<li>PostServlet：发帖</li>
<li>ReplyServlet：回复</li>
</ul>
<p>其中ProfileServlet、PostServlet和ReplyServlet都需要用户登录后才能操作，否则应当直接跳转到登录页面。我们可以直接把登录逻辑写到这3个Servlet中，但是同样的逻辑重复3次没有必要，并且，如果后续继续加Servlet并且也需要验证登录时，还继续要重复这个登录逻辑。</p>
<p>为了把一些公用逻辑从各个Servlet中抽离出来，JavaEE的Servlet规范还提供了一种Filter组件，即过滤器。它的作用是在HTTP请求到达Servlet之前，可以被一个或多个Filter处理。类似打印日志、登录检查等逻辑，完全可以放到Filter中。</p>
<p>例如，我们编写一个最简单的EncodingFilter，它强制把输入和输出的编码设置为UTF-8：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter(urlPatterns = "/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EncodingFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        System.out.println(<span class="hljs-string">"EncodingFilter:doFilter"</span>);<br>        request.setCharacterEncoding(<span class="hljs-string">"UTF-8"</span>);<br>        response.setCharacterEncoding(<span class="hljs-string">"UTF-8"</span>);<br>        chain.doFilter(request, response);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>编写Filter时，必须实现<code>Filter</code>接口，在<code>doFilter()</code>方法内部，要继续处理请求，必须调用<code>chain.doFilter()</code>。最后，用<code>@WebFilter</code>注解标注该Filter需要过滤的URL，这里的<code>/*</code>表示所有路径。</p>
<p>添加了Filter后，整个请求的处理架构如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs ascii">            ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐<br>                                   /             ┌──────────────┐<br>            │                     ┌─────────────&gt;│ IndexServlet │ │<br>                                  │              └──────────────┘<br>            │                     │/signin       ┌──────────────┐ │<br>                                  ├─────────────&gt;│SignInServlet │<br>            │                     │              └──────────────┘ │<br>                                  │/signout      ┌──────────────┐<br>┌───────┐   │   ┌──────────────┐  ├─────────────&gt;│SignOutServlet│ │<br>│Browser│──────&gt;│EncodingFilter├──┤              └──────────────┘<br>└───────┘   │   └──────────────┘  │/user/profile ┌──────────────┐ │<br>                                  ├─────────────&gt;│ProfileServlet│<br>            │                     │              └──────────────┘ │<br>                                  │/user/post    ┌──────────────┐<br>            │                     ├─────────────&gt;│ PostServlet  │ │<br>                                  │              └──────────────┘<br>            │                     │/user/reply   ┌──────────────┐ │<br>                                  └─────────────&gt;│ ReplyServlet │<br>            │                                    └──────────────┘ │<br>             ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─<br></code></pre></td></tr></tbody></table></figure>

<p>还可以继续添加其他Filter，例如LogFilter：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter("/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LogFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        System.out.println(<span class="hljs-string">"LogFilter: process "</span> + ((HttpServletRequest) request).getRequestURI());<br>        chain.doFilter(request, response);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>多个Filter会组成一个链，每个请求都被链上的Filter依次处理：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                                        ┌────────┐<br>                                     ┌─&gt;│ServletA│<br>                                     │  └────────┘<br>    ┌──────────────┐    ┌─────────┐  │  ┌────────┐<br>───&gt;│EncodingFilter│───&gt;│LogFilter│──┼─&gt;│ServletB│<br>    └──────────────┘    └─────────┘  │  └────────┘<br>                                     │  ┌────────┐<br>                                     └─&gt;│ServletC│<br>                                        └────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>那么有多个Filter时，Filter的顺序如何指定？多个Filter按不同的顺序处理会造成处理结果不同吗？答案是Filter的顺序确实对处理的结果有影响，但遗憾的是，Servlet规范并没有对<code>@WebFilter</code>注解标注的Filter规定顺序。如果一定要给每个Filter指定顺序，就必须在<code>web.xml</code>中对这些Filter再配置一遍。</p>
<p>注意到上述两个Filter的过滤路径都是<code>/*</code>，即它们会对所有请求进行过滤。也可以编写只对特定路径进行过滤的Filter，例如<code>AuthFilter</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter("/user/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        System.out.println(<span class="hljs-string">"AuthFilter: check authentication"</span>);<br>        HttpServletRequest req = (HttpServletRequest) request;<br>        HttpServletResponse resp = (HttpServletResponse) response;<br>        <span class="hljs-keyword">if</span> (req.getSession().getAttribute(<span class="hljs-string">"user"</span>) == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 未登录，自动跳转到登录页:</span><br>            System.out.println(<span class="hljs-string">"AuthFilter: not signin!"</span>);<br>            resp.sendRedirect(<span class="hljs-string">"/signin"</span>);<br>        } <span class="hljs-keyword">else</span> {<br>            <span class="hljs-comment">// 已登录，继续处理:</span><br>            chain.doFilter(request, response);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到<code>AuthFilter</code>只过滤以<code>/user/</code>开头的路径，因此：</p>
<ul>
<li>如果一个请求路径类似<code>/user/profile</code>，那么它会被上述3个Filter依次处理</li>
<li>如果一个请求路径类似<code>/test</code>，那么它会被上述2个Filter依次处理（不会被AuthFilter处理）</li>
</ul>
<p>再注意观察<code>AuthFilter</code>，当用户没有登录时，在<code>AuthFilter</code>内部，直接调用<code>resp.sendRedirect()</code>发送重定向，且没有调用<code>chain.doFilter()</code>，因此，当用户没有登录时，请求到达<code>AuthFilter</code>后，不再继续处理，即后续的Filter和任何Servlet都没有机会处理该请求了。</p>
<p>可见，Filter可以针对性地拦截或放行HTTP请求。</p>
<p>如果一个Filter在当前请求中生效，但什么都没有做：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter("/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        <span class="hljs-comment">// TODO</span><br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>那么，用户将看到一个空白页，因为请求没有继续处理，默认响应是200加空白输出。**如果Filter要使请求继续被处理，就一定要调用<code>chain.doFilter()</code>**。</p>
<p>总结一下，Filter是一种对HTTP请求进行预处理的组件，他可以构成一个处理链，使得公共代码能集中到一起。Filter适用于日志、登录检查、全局设置等。设计合理的URL映射可以让Filter链更清晰。</p>
<h4 id="修改请求"><a href="#修改请求" class="headerlink" title="修改请求"></a>修改请求</h4><p>Filter可以对请求进行预处理，因此，我们可以把很多公共预处理逻辑放到Filter中完成。考察这样一种需求：我们在Web应用中经常需要处理用户上传文件，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/upload/file")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UploadServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doPost</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        <span class="hljs-comment">// 读取Request Body:</span><br>        InputStream input = req.getInputStream();<br>        ByteArrayOutputStream output = <span class="hljs-keyword">new</span> ByteArrayOutputStream();<br>        <span class="hljs-keyword">byte</span>[] buffer = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">1024</span>];<br>        <span class="hljs-keyword">for</span> (;;) {<br>            <span class="hljs-keyword">int</span> len = input.read(buffer);<br>            <span class="hljs-keyword">if</span> (len == -<span class="hljs-number">1</span>) {<br>                <span class="hljs-keyword">break</span>;<br>            }<br>            output.write(buffer, <span class="hljs-number">0</span>, len);<br>        }<br>        <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 写入文件:</span><br>        <span class="hljs-comment">// 显示上传结果:</span><br>        String uploadedText = output.toString(StandardCharsets.UTF_8);<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Uploaded:&lt;/h1&gt;"</span>);<br>        pw.write(<span class="hljs-string">"&lt;pre&gt;&lt;code&gt;"</span>);<br>        pw.write(uploadedText);<br>        pw.write(<span class="hljs-string">"&lt;/code&gt;&lt;/pre&gt;"</span>);<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>要保证文件上传的一致性，我们在上传文件时，把文件的哈希也传上去，服务端做一个验证，就可以确保用户上传的文件一定是完整的。这个验证逻辑非常适合写在<code>ValidateUploadFilter</code>中，因为它可以复用，如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter("/upload/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ValidateUploadFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        HttpServletRequest req = (HttpServletRequest) request;<br>        HttpServletResponse resp = (HttpServletResponse) response;<br>        <span class="hljs-comment">// 获取客户端传入的签名方法和签名:</span><br>        String digest = req.getHeader(<span class="hljs-string">"Signature-Method"</span>);<br>        String signature = req.getHeader(<span class="hljs-string">"Signature"</span>);<br>        <span class="hljs-keyword">if</span> (digest == <span class="hljs-keyword">null</span> || digest.isEmpty() || signature == <span class="hljs-keyword">null</span> || signature.isEmpty()) {<br>            sendErrorPage(resp, <span class="hljs-string">"Missing signature."</span>);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 读取Request的Body并验证签名:</span><br>        MessageDigest md = getMessageDigest(digest);<br>        InputStream input = <span class="hljs-keyword">new</span> DigestInputStream(request.getInputStream(), md);<br>        <span class="hljs-keyword">byte</span>[] buffer = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">1024</span>];<br>        <span class="hljs-keyword">for</span> (;;) {<br>            <span class="hljs-keyword">int</span> len = input.read(buffer);<br>            <span class="hljs-keyword">if</span> (len == -<span class="hljs-number">1</span>) {<br>                <span class="hljs-keyword">break</span>;<br>            }<br>        }<br>        String actual = toHexString(md.digest());<br>        <span class="hljs-keyword">if</span> (!signature.equals(actual)) {<br>            sendErrorPage(resp, <span class="hljs-string">"Invalid signature."</span>);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 验证成功后继续处理:</span><br>        chain.doFilter(request, response);<br>    }<br><br>    <span class="hljs-comment">// 将byte[]转换为hex string:</span><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> String <span class="hljs-title">toHexString</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] digest)</span> </span>{<br>        StringBuilder sb = <span class="hljs-keyword">new</span> StringBuilder();<br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">byte</span> b : digest) {<br>            sb.append(String.format(<span class="hljs-string">"%02x"</span>, b));<br>        }<br>        <span class="hljs-keyword">return</span> sb.toString();<br>    }<br><br>    <span class="hljs-comment">// 根据名称创建MessageDigest:</span><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> MessageDigest <span class="hljs-title">getMessageDigest</span><span class="hljs-params">(String name)</span> <span class="hljs-keyword">throws</span> ServletException </span>{<br>        <span class="hljs-keyword">try</span> {<br>            <span class="hljs-keyword">return</span> MessageDigest.getInstance(name);<br>        } <span class="hljs-keyword">catch</span> (NoSuchAlgorithmException e) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ServletException(e);<br>        }<br>    }<br><br>    <span class="hljs-comment">// 发送一个错误响应:</span><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sendErrorPage</span><span class="hljs-params">(HttpServletResponse resp, String errorMessage)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;html&gt;&lt;body&gt;&lt;h1&gt;"</span>);<br>        pw.write(errorMessage);<br>        pw.write(<span class="hljs-string">"&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;"</span>);<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>ValidateUploadFilter对签名进行验证的逻辑是没有问题的，但是UploadServlet并为读取到任何数据。这是因为对HttpServletRequest进行读取时，只能读取一次。如果Filter调用getInputStream()读取了一次数据，后续Servlet处理时，再次读取将无法读到任何数据，咋么办？这时，我们需要“伪造”一个HttpServletRequest，具体做法是使用代理模式，对<code>getInputStream()</code>和<code>getReader()</code>返回一个新的流：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReReadableHttpServletRequest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServletRequestWrapper</span> </span>{<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">byte</span>[] body;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> open = <span class="hljs-keyword">false</span>;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ReReadableHttpServletRequest</span><span class="hljs-params">(HttpServletRequest request, <span class="hljs-keyword">byte</span>[] body)</span> </span>{<br>        <span class="hljs-keyword">super</span>(request);<br>        <span class="hljs-keyword">this</span>.body = body;<br>    }<br><br>    <span class="hljs-comment">// 返回InputStream:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ServletInputStream <span class="hljs-title">getInputStream</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">if</span> (open) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Cannot re-open input stream!"</span>);<br>        }<br>        open = <span class="hljs-keyword">true</span>;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ServletInputStream() {<br>            <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> offset = <span class="hljs-number">0</span>;<br><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isFinished</span><span class="hljs-params">()</span> </span>{<br>                <span class="hljs-keyword">return</span> offset &gt;= body.length;<br>            }<br><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isReady</span><span class="hljs-params">()</span> </span>{<br>                <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;<br>            }<br><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setReadListener</span><span class="hljs-params">(ReadListener listener)</span> </span>{<br>            }<br><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>                <span class="hljs-keyword">if</span> (offset &gt;= body.length) {<br>                    <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span>;<br>                }<br>                <span class="hljs-keyword">int</span> n = body[offset] &amp; <span class="hljs-number">0xff</span>;<br>                offset++;<br>                <span class="hljs-keyword">return</span> n;<br>            }<br>        };<br>    }<br><br>    <span class="hljs-comment">// 返回Reader:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> BufferedReader <span class="hljs-title">getReader</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">if</span> (open) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Cannot re-open reader!"</span>);<br>        }<br>        open = <span class="hljs-keyword">true</span>;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BufferedReader(<span class="hljs-keyword">new</span> InputStreamReader(<span class="hljs-keyword">new</span> ByteArrayInputStream(body), <span class="hljs-string">"UTF-8"</span>));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意观察<code>ReReadableHttpServletRequest</code>的构造方法，它保存了<code>ValidateUploadFilter</code>读取的<code>byte[]</code>，并在调用<code>getInputStream()</code>时通过<code>byte[]</code>构造了一个新的<code>ServletInputStream</code>。然后，我们在ValidateUploadFilter中，把doFilter()调用时传给下一个处理者的<code>HttpServletRequest</code>替换成我们自己“伪造”的<code>ReReadableHttpServletRequest</code>。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">        <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>    ...<br>    chain.doFilter(<span class="hljs-keyword">new</span> ReReadableHttpServletRequest(req, output.toByteArray()), response);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>再注意到我们编写<code>ReReadableHttpServletRequest</code>时，是从<code>HttpServletRequestWrapper</code>继承，而不是直接实现<code>HttpServletRequest</code>接口。这是因为，Servlet的每个新版本都会对接口增加一些新方法，从<code>HttpServletRequestWrapper</code>继承可以确保新方法被正确地覆写了，因为<code>HttpServletRequestWrapper</code>是由Servlet的jar包提供的，目的就是为了让我们方便地实现对<code>HttpServletRequest</code>接口的代理。</p>
<p>我们总结一下对<code>HttpServletRequest</code>接口进行代理的步骤：</p>
<ol>
<li>从<code>HttpServletRequestWrapper</code>继承一个<code>XxxHttpServletRequest</code>，需要传入原始的<code>HttpServletRequest</code>实例；</li>
<li>覆写某些方法，使得新的<code>XxxHttpServletRequest</code>实例看上去“改变”了原始的<code>HttpServletRequest</code>实例；</li>
<li>在<code>doFilter()</code>中传入新的<code>XxxHttpServletRequest</code>实例。</li>
</ol>
<p>虽然整个Filter的代码比较复杂，但它的好处在于：这个Filter在整个处理链中实现了灵活的“可插拔”特性，即是否启用对Web应用程序的其他组件（Filter、Servlet）完全没有影响。</p>
<h4 id="修改响应"><a href="#修改响应" class="headerlink" title="修改响应"></a>修改响应</h4><p>我们来看一下什么时候需要修改<code>HttpServletResponse</code>。假设我们编写了一个Servlet，但由于业务逻辑比较复杂，处理该请求需要耗费很长时间。好消息是每次返回的响应内容是固定的，因此，如果我们能把结果缓存起来，就可以大大提高Web应用程序的运行效率。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/slow/hello")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        resp.setContentType(<span class="hljs-string">"text/html"</span>);<br>        <span class="hljs-comment">// 模拟耗时1秒:</span><br>        <span class="hljs-keyword">try</span> {<br>            Thread.sleep(<span class="hljs-number">1000</span>);<br>        } <span class="hljs-keyword">catch</span> (InterruptedException e) {<br>        }<br>        PrintWriter pw = resp.getWriter();<br>        pw.write(<span class="hljs-string">"&lt;h1&gt;Hello, world!&lt;/h1&gt;"</span>);<br>        pw.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>缓存逻辑最好不要在Servlet内部实现，因为我们希望能复用缓存逻辑，编写一个<code>CacheFilter</code>最合适。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebFilter("/slow/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CacheFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-comment">// Path到byte[]的缓存:</span><br>    <span class="hljs-keyword">private</span> Map&lt;String, <span class="hljs-keyword">byte</span>[]&gt; cache = <span class="hljs-keyword">new</span> ConcurrentHashMap&lt;&gt;();<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        HttpServletRequest req = (HttpServletRequest) request;<br>        HttpServletResponse resp = (HttpServletResponse) response;<br>        <span class="hljs-comment">// 获取Path:</span><br>        String url = req.getRequestURI();<br>        <span class="hljs-comment">// 获取缓存内容:</span><br>        <span class="hljs-keyword">byte</span>[] data = <span class="hljs-keyword">this</span>.cache.get(url);<br>        resp.setHeader(<span class="hljs-string">"X-Cache-Hit"</span>, data == <span class="hljs-keyword">null</span> ? <span class="hljs-string">"No"</span> : <span class="hljs-string">"Yes"</span>);<br>        <span class="hljs-keyword">if</span> (data == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 缓存未找到,构造一个伪造的Response:</span><br>            CachedHttpServletResponse wrapper = <span class="hljs-keyword">new</span> CachedHttpServletResponse(resp);<br>            <span class="hljs-comment">// 让下游组件写入数据到伪造的Response:</span><br>            chain.doFilter(request, wrapper);<br>            <span class="hljs-comment">// 从伪造的Response中读取写入的内容并放入缓存:</span><br>            data = wrapper.getContent();<br>            cache.put(url, data);<br>        }<br>        <span class="hljs-comment">// 写入到原始的Response:</span><br>        ServletOutputStream output = resp.getOutputStream();<br>        output.write(data);<br>        output.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>实现缓存的关键在于，调用<code>doFilter()</code>时，我们不能传入原始的<code>HttpServletResponse</code>，因为这样就会写入Socket，我们也就无法获取下游组件写入的内容。如果我们传入的是“伪造”的<code>HttpServletResponse</code>，让下游组件写入到我们预设的<code>ByteArrayOutputStream</code>，我们就“截获”了下游组件写入的内容，于是，可以把内容缓存起来，再通过原始的<code>HttpServletResponse</code>实例写入到网络。</p>
<p>这个<code>CachedHttpServletResponse</code>实现如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CachedHttpServletResponse</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServletResponseWrapper</span> </span>{<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> open = <span class="hljs-keyword">false</span>;<br>    <span class="hljs-keyword">private</span> ByteArrayOutputStream output = <span class="hljs-keyword">new</span> ByteArrayOutputStream();<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">CachedHttpServletResponse</span><span class="hljs-params">(HttpServletResponse response)</span> </span>{<br>        <span class="hljs-keyword">super</span>(response);<br>    }<br><br>    <span class="hljs-comment">// 获取Writer:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> PrintWriter <span class="hljs-title">getWriter</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">if</span> (open) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Cannot re-open writer!"</span>);<br>        }<br>        open = <span class="hljs-keyword">true</span>;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> PrintWriter(output, <span class="hljs-keyword">false</span>, StandardCharsets.UTF_8);<br>    }<br><br>    <span class="hljs-comment">// 获取OutputStream:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ServletOutputStream <span class="hljs-title">getOutputStream</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">if</span> (open) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalStateException(<span class="hljs-string">"Cannot re-open output stream!"</span>);<br>        }<br>        open = <span class="hljs-keyword">true</span>;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ServletOutputStream() {<br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isReady</span><span class="hljs-params">()</span> </span>{<br>                <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;<br>            }<br><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setWriteListener</span><span class="hljs-params">(WriteListener listener)</span> </span>{<br>            }<br><br>            <span class="hljs-comment">// 实际写入ByteArrayOutputStream:</span><br>            <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">write</span><span class="hljs-params">(<span class="hljs-keyword">int</span> b)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>                output.write(b);<br>            }<br>        };<br>    }<br><br>    <span class="hljs-comment">// 返回写入的byte[]:</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">byte</span>[] getContent() {<br>        <span class="hljs-keyword">return</span> output.toByteArray();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>可见，如果我们想要修改响应，就可以通过<code>HttpServletResponseWrapper</code>构造一个“伪造”的<code>HttpServletResponse</code>，这样就能拦截到写入的数据。修改响应后，不要忘记把数据写入原始的<code>HttpServletResponse</code>实例。</p>
<p>这个<code>CacheFilter</code>同样是一个“可拔插”组件，它是否启用不影响Web应用程序的其他组件（Filter，Servlet）。</p>
<h3 id="使用Listener"><a href="#使用Listener" class="headerlink" title="使用Listener"></a>使用Listener</h3><p>除了Servlet和Filter，JavaEE的Servlet规范还提供了第三种组件：Listener。Listener叫做监听器，有好几种Listener，其中最常用的是<code>ServletContextListener</code>，我们编写一个实现了<code>ServletContextListener</code>接口的类如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebListener</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppListener</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ServletContextListener</span> </span>{<br>    <span class="hljs-comment">// 在此初始化WebApp,例如打开数据库连接池等:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">contextInitialized</span><span class="hljs-params">(ServletContextEvent sce)</span> </span>{<br>        System.out.println(<span class="hljs-string">"WebApp initialized."</span>);<br>    }<br><br>    <span class="hljs-comment">// 在此清理WebApp,例如关闭数据库连接池等:</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">contextDestroyed</span><span class="hljs-params">(ServletContextEvent sce)</span> </span>{<br>        System.out.println(<span class="hljs-string">"WebApp destroyed."</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>任何标注为<code>@WebListener</code>，且实现了特定接口的类，都会被Web服务器自动初始化。上述<code>AppListener</code>实现了<code>ServletContextListener</code>接口，它会在整个Web应用程序初始化完成后，以及Web应用程序关闭后获得回调通知。我们可以把初始化数据库连接池等工作放到<code>contextInitialized()</code>回调方法中，把清理资源的工作放到<code>contextDestroyed()</code>回调方法中，因为Web服务器保证在<code>contextInitialized()</code>执行后，才会接受用户的HTTP请求。</p>
<p>很多第三方Web框架都会通过一个ServletContextListener接口初始化自己。此外，还有几种Listener：</p>
<ul>
<li>HttpSessionListener：监听HttpSession的创建和销毁事件</li>
<li>ServletRequestListener：监听ServletRequest请求的创建和销毁事件</li>
<li>ServletRequestAttributeListener：监听ServletRequest请求的属性变化事件（即调用<code>ServletRequest.setAttribute()</code>方法）</li>
<li>ServletContextAttributeListener：监听ServletContext的属性变化事件（即调用<code>ServletContext.setAttribute()</code>方法）</li>
</ul>
<h4 id="ServletContext"><a href="#ServletContext" class="headerlink" title="ServletContext"></a>ServletContext</h4><p>一个Web服务器可以运行一个或多个WebApp，对于每个WebApp，Web服务器都会为其创建一个全局唯一的ServletContext实例，我们在AppListener里面编写的两个回调方法，实际上对应的就是ServletContext的创建和销毁。</p>
<p><code>ServletRequest</code>、<code>HttpSession</code>等很多对象也提供<code>getServletContext()</code>方法获取到同一个<code>ServletContext</code>实例，<code>ServletContext</code>是一个WebApp运行期的全局唯一实例。<code>ServletContext</code>实例最大的作用就是设置和共享全局信息。此外，<code>ServletContext</code>还提供了动态添加Servlet、Filter、Listener等功能，它允许应用程序在运行期间动态添加一个组件，虽然这个功能不是很常用。</p>
<h3 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h3><p>对一个Web应用程序来说，除了Servlet、Filter这些逻辑组件，还需要JSP这样的视图文件，外加一堆静态资源文件，如CSS、JS等。</p>
<p>合理组织文件结构非常重要。我们以一个具体的Web应用程序为例：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs ascii">webapp<br>├── pom.xml<br>└── src<br>    └── main<br>        ├── java<br>        │   └── com<br>        │       └── itranswarp<br>        │           └── learnjava<br>        │               ├── Main.java<br>        │               ├── filter<br>        │               │   └── EncodingFilter.java<br>        │               └── servlet<br>        │                   ├── FileServlet.java<br>        │                   └── HelloServlet.java<br>        ├── resources<br>        └── webapp<br>            ├── WEB-INF<br>            │   └── web.xml<br>            ├── favicon.ico<br>            └── static<br>                └── bootstrap.css<br></code></pre></td></tr></tbody></table></figure>

<p>我们把所有的静态资源文件放入<code>/static/</code>目录，在开发阶段，有些Web服务器会自动为我们加一个专门负责处理静态文件的Servlet，但如果<code>IndexServlet</code>映射路径为<code>/</code>，会屏蔽掉处理静态文件的Servlet映射。因此，我们需要自己编写一个处理静态文件的<code>FileServlet</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WebServlet(urlPatterns = "/static/*")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FileServlet</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpServlet</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doGet</span><span class="hljs-params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="hljs-keyword">throws</span> ServletException, IOException </span>{<br>        ServletContext ctx = req.getServletContext();<br>        <span class="hljs-comment">// RequestURI包含ContextPath,需要去掉:</span><br>        String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());<br>        <span class="hljs-comment">// 获取真实文件路径:</span><br>        String filepath = ctx.getRealPath(urlPath);<br>        <span class="hljs-keyword">if</span> (filepath == <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 无法获取到路径:</span><br>            resp.sendError(HttpServletResponse.SC_NOT_FOUND);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        Path path = Paths.get(filepath);<br>        <span class="hljs-keyword">if</span> (!path.toFile().isFile()) {<br>            <span class="hljs-comment">// 文件不存在:</span><br>            resp.sendError(HttpServletResponse.SC_NOT_FOUND);<br>            <span class="hljs-keyword">return</span>;<br>        }<br>        <span class="hljs-comment">// 根据文件名猜测Content-Type:</span><br>        String mime = Files.probeContentType(path);<br>        <span class="hljs-keyword">if</span> (mime == <span class="hljs-keyword">null</span>) {<br>            mime = <span class="hljs-string">"application/octet-stream"</span>;<br>        }<br>        resp.setContentType(mime);<br>        <span class="hljs-comment">// 读取文件并写入Response:</span><br>        OutputStream output = resp.getOutputStream();<br>        <span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">new</span> BufferedInputStream(<span class="hljs-keyword">new</span> FileInputStream(filepath))) {<br>            input.transferTo(output);<br>        }<br>        output.flush();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这样一来，在开发阶段，我们就可以方便地高效开发。</p>
<p>类似Tomcat这样的Web服务器，运行的Web应用程序通常都是业务系统，因此，这类服务器也被称为应用服务器。应用服务器并不擅长处理静态文件，也不适合直接暴露给用户。通常，我们在生产环境部署时，总是使用类似Nginx这样的服务器充当反向代理和静态服务器，只有动态请求才会放行给应用服务器，所以，部署架构如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs ascii">             ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐<br><br>             │  /static/*            │<br>┌───────┐      ┌──────────&gt; file<br>│Browser├────┼─┤                     │    ┌ ─ ─ ─ ─ ─ ─ ┐<br>└───────┘      │/          proxy_pass<br>             │ └─────────────────────┼───&gt;│  Web Server │<br>                       Nginx<br>             └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘    └ ─ ─ ─ ─ ─ ─ ┘<br></code></pre></td></tr></tbody></table></figure>

<p>实现上述功能的Nginx配置文件如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">server {<br>    listen 80;<br><br>    server_name www.local.liaoxuefeng.com;<br><br>    # 静态文件根目录:<br>    root /path/to/src/main/webapp;<br><br>    access_log /var/log/nginx/webapp_access_log;<br>    error_log  /var/log/nginx/webapp_error_log;<br><br>    # 处理静态文件请求:<br>    location /static {<br>    }<br><br>    # 处理静态文件请求:<br>    location /favicon.ico {<br>    }<br><br>    # 不允许请求/WEB-INF:<br>    location /WEB-INF {<br>        return 404;<br>    }<br><br>    # 其他请求转发给Tomcat:<br>    location / {<br>        proxy_pass       http://127.0.0.1:8080;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>        proxy_set_header X-Forwarded-Proto $scheme;<br>        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>使用Nginx配合Tomcat服务器，可以充分发挥Nginx作为网关的优势，既可以高效处理静态文件，也可以把https、防火墙、限速、反爬虫等功能放到Nginx中，使得我们自己的WebApp能专注于业务逻辑。</p>
<p>部署Web应用程序时，要设计合理的目录结构，同时考虑开发模式需要便捷性，生产模式需要高性能。</p>
</body></html>
    
    </div>
    
    
    <div class="columns is-mobile is-multiline article-nav">
        <span class="column is-12-mobile is-half-desktop  article-nav-prev">
            
            <a href="/Study/English/zh-cn/Confidence/">Confidence</a>
            
        </span>
        <span class="column is-12-mobile is-half-desktop  article-nav-next">
            
            <a href="/Study/Java/zh-cn/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/">设计模式</a>
            
        </span>
    </div>
    
</article>


<div class="sharebox">
    
<div class="sharethis-inline-share-buttons"></div>
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=608c1408daac690012507aa2&amp;product=sop' async='async'></script>

</div>



    </div>
</section>
    <footer class="footer">
    <div class="container">
        <div class="columns content">
            <div class="column is-narrow has-text-centered">
                &copy; 2021 wanzixin&nbsp;
                Powered by <a href="http://hexo.io/" target="_blank">Hexo</a> & <a
                        target="_blank" rel="noopener" href="http://github.com/ppoffice/hexo-theme-minos">Minos</a>
            </div>
            <div class="column is-hidden-mobile"></div>

            
            <div class="column is-narrow">
                <div class="columns is-mobile is-multiline is-centered">
                
                    
                <a class="column is-narrow has-text-black" title="GitHub" target="_blank" rel="noopener" href="https://github.com/ppoffice/hexo-theme-minos">
                    
                    GitHub
                    
                </a>
                
                </div>
            </div>
            
            
<div class="column is-narrow has-text-centered">
    <div class="dropdown is-up is-right is-hoverable" style="margin-top: -0.2em;">
        <div class="dropdown-trigger">
            <button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu7">
                <span class="icon">
                    <i class="fas fa-globe"></i>
                </span>
                <span>简体中文</span>
                <span class="icon is-small">
            <i class="fas fa-angle-down" aria-hidden="true"></i>
          </span>
            </button>
        </div>
        <div class="dropdown-menu has-text-left" role="menu">
            <div class="dropdown-content">
            
                <a href="/Study/Java/zh-cn/Web%E5%BC%80%E5%8F%91/" class="dropdown-item">
                    English
                </a>
            
                <a href="/zh-cn/Study/Java/zh-cn/Web%E5%BC%80%E5%8F%91/" class="dropdown-item">
                    简体中文
                </a>
            
            </div>
        </div>
    </div>
</div>

        </div>
    </div>
</footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment-with-locales.min.js"></script>

<!-- test if the browser is outdated -->
<div id="outdated">
    <h6>Your browser is out-of-date!</h6>
    <p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" target="_blank" rel="noopener" href="http://outdatedbrowser.com/">Update my browser now </a></p>
    <p class="last"><a href="#" id="btnCloseUpdateBrowser" title="Close">&times;</a></p>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.js"></script>
<script>
    $(document).ready(function () {
        // plugin function, place inside DOM ready function
        outdatedBrowser({
            bgColor: '#f25648',
            color: '#ffffff',
            lowerThan: 'flex'
        })
    });
</script>

<script>
    window.FontAwesomeConfig = {
        searchPseudoElements: true
    }
    moment.locale("zh-CN");
</script>


    
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script>
    MathJax.Hub.Config({
        "HTML-CSS": {
            matchFontHeight: false
        },
        SVG: {
            matchFontHeight: false
        },
        CommonHTML: {
            matchFontHeight: false
        },
        tex2jax: {
            inlineMath: [
                ['$','$'],
                ['\\(','\\)']
            ]
        }
    });
</script>

    
    
    
    
<script src="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/js/lightgallery-all.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/js/jquery.justifiedGallery.min.js"></script>
<script>
    (function ($) {
        $(document).ready(function () {
            if (typeof($.fn.lightGallery) === 'function') {
                $('.article.gallery').lightGallery({ selector: '.gallery-item' });
            }
            if (typeof($.fn.justifiedGallery) === 'function') {
                $('.justified-gallery').justifiedGallery();
            }
        });
    })(jQuery);
</script>

    
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
    <style>
        .hljs {
            position: relative;
        }

        .hljs .clipboard-btn {
            float: right;
            color: #9a9a9a;
            background: none;
            border: none;
            cursor: pointer;
        }

        .hljs .clipboard-btn:hover {
          color: #8a8a8a;
        }

        .hljs > .clipboard-btn {
            display: none;
            position: absolute;
            right: 4px;
            top: 4px;
        }

        .hljs:hover > .clipboard-btn {
            display: inline;
        }

        .hljs > figcaption > .clipboard-btn {
            margin-right: 4px;
        }
    </style>
    <script>
      $(document).ready(function () {
        $('figure.hljs').each(function(i, figure) {
          var codeId = 'code-' + i;
          var code = figure.querySelector('.code');
          var copyButton = $('<button>Copy <i class="far fa-clipboard"></i></button>');
          code.id = codeId;
          copyButton.addClass('clipboard-btn');
          copyButton.attr('data-clipboard-target-id', codeId);

          var figcaption = figure.querySelector('figcaption');

          if (figcaption) {
            figcaption.append(copyButton[0]);
          } else {
            figure.prepend(copyButton[0]);
          }
        })

        var clipboard = new ClipboardJS('.clipboard-btn', {
          target: function(trigger) {
            return document.getElementById(trigger.getAttribute('data-clipboard-target-id'));
          }
        });
        clipboard.on('success', function(e) {
          e.clearSelection();
        })
      })
    </script>

    
    

    



<script src="/js/script.js"></script>


    
    <div class="searchbox ins-search">
    <div class="searchbox-mask"></div>
    <div class="searchbox-container ins-search-container">
        <div class="searchbox-input-wrapper">
            <input type="text" class="searchbox-input ins-search-input" placeholder="站内搜索" />
            <span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
        </div>
        <div class="searchbox-result-wrapper ins-section-wrapper">
            <div class="ins-section-container"></div>
        </div>
    </div>
</div>
<script>
    (function (window) {
        var INSIGHT_CONFIG = {
            TRANSLATION: {
                POSTS: '文章',
                PAGES: '页面',
                CATEGORIES: '分类',
                TAGS: '标签',
                UNTITLED: '(无标题)',
            },
            CONTENT_URL: '/content.zh-cn.json',
        };
        window.INSIGHT_CONFIG = INSIGHT_CONFIG;
    })(window);
</script>

<script src="/js/insight.js"></script>

    
</body>
</html>